Merge "CheckOperationsImpl#notesAsText: Don't create 2 ObjectReaders"
diff --git a/java/com/google/gerrit/plugins/checks/Checker.java b/java/com/google/gerrit/plugins/checks/Checker.java
index 56fa64b..075e2c9 100644
--- a/java/com/google/gerrit/plugins/checks/Checker.java
+++ b/java/com/google/gerrit/plugins/checks/Checker.java
@@ -15,37 +15,18 @@
 package com.google.gerrit.plugins.checks;
 
 import com.google.auto.value.AutoValue;
-import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSortedSet;
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.index.query.IndexPredicate;
-import com.google.gerrit.index.query.Predicate;
-import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.plugins.checks.api.BlockingCondition;
 import com.google.gerrit.plugins.checks.api.CheckerStatus;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.query.change.ChangeQueryBuilder;
-import com.google.gerrit.server.query.change.ChangeQueryProcessor;
-import com.google.gerrit.server.query.change.ChangeStatusPredicate;
-import com.google.gerrit.server.query.change.ProjectPredicate;
-import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.update.RetryHelper.ActionType;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Provider;
 import java.sql.Timestamp;
-import java.util.List;
 import java.util.Optional;
-import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.ObjectId;
 
 /** Definition of a checker. */
 @AutoValue
 public abstract class Checker {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
   /**
    * Returns the UUID of the checker.
    *
@@ -145,91 +126,6 @@
     return CheckerStatus.DISABLED == getStatus();
   }
 
-  public boolean isCheckerRelevant(ChangeData cd, ChangeQueryBuilder changeQueryBuilder)
-      throws OrmException {
-    if (!getQuery().isPresent()) {
-      return cd.change().isNew();
-    }
-
-    Predicate<ChangeData> predicate;
-    try {
-      predicate = createQueryPredicate(changeQueryBuilder);
-    } catch (ConfigInvalidException e) {
-      logger.atWarning().withCause(e).log("skipping invalid query for checker %s", getUuid());
-      return false;
-    }
-
-    return predicate.asMatchable().match(cd);
-  }
-
-  public List<ChangeData> queryMatchingChanges(
-      RetryHelper retryHelper,
-      ChangeQueryBuilder changeQueryBuilder,
-      Provider<ChangeQueryProcessor> changeQueryProcessorProvider)
-      throws ConfigInvalidException, OrmException {
-    return executeIndexQueryWithRetry(
-        retryHelper, changeQueryProcessorProvider, createQueryPredicate(changeQueryBuilder));
-  }
-
-  private Predicate<ChangeData> createQueryPredicate(ChangeQueryBuilder changeQueryBuilder)
-      throws ConfigInvalidException {
-    Predicate<ChangeData> predicate = new ProjectPredicate(getRepository().get());
-
-    if (getQuery().isPresent()) {
-      String query = getQuery().get();
-      Predicate<ChangeData> predicateForQuery;
-      try {
-        predicateForQuery = changeQueryBuilder.parse(query);
-      } catch (QueryParseException e) {
-        throw new ConfigInvalidException(
-            String.format("change query of checker %s is invalid: %s", getUuid(), query), e);
-      }
-
-      if (!predicateForQuery.isMatchable()) {
-        // Assuming nobody modified the query behind Gerrit's back, this is programmer error:
-        // CheckerQuery should not be able to produce non-matchable queries.
-        throw new ConfigInvalidException(
-            String.format("change query of checker %s is non-matchable: %s", getUuid(), query));
-      }
-
-      predicate = Predicate.and(predicate, predicateForQuery);
-    }
-
-    if (!hasStatusPredicate(predicate)) {
-      predicate = Predicate.and(ChangeStatusPredicate.open(), predicate);
-    }
-
-    return predicate;
-  }
-
-  private static boolean hasStatusPredicate(Predicate<ChangeData> predicate) {
-    if (predicate instanceof IndexPredicate) {
-      return ((IndexPredicate<ChangeData>) predicate)
-          .getField()
-          .getName()
-          .equals(ChangeField.STATUS.getName());
-    }
-    return predicate.getChildren().stream().anyMatch(Checker::hasStatusPredicate);
-  }
-
-  // TODO(ekempin): Retrying the query should be done by ChangeQueryProcessor.
-  private List<ChangeData> executeIndexQueryWithRetry(
-      RetryHelper retryHelper,
-      Provider<ChangeQueryProcessor> changeQueryProcessorProvider,
-      Predicate<ChangeData> predicate)
-      throws OrmException {
-    try {
-      return retryHelper.execute(
-          ActionType.INDEX_QUERY,
-          () -> changeQueryProcessorProvider.get().query(predicate).entities(),
-          OrmException.class::isInstance);
-    } catch (Exception e) {
-      Throwables.throwIfUnchecked(e);
-      Throwables.throwIfInstanceOf(e, OrmException.class);
-      throw new OrmException(e);
-    }
-  }
-
   /**
    * Checks whether a {@link Checker} is required for submission or not.
    *
diff --git a/java/com/google/gerrit/plugins/checks/CheckerQuery.java b/java/com/google/gerrit/plugins/checks/CheckerQuery.java
index 5712c93..5bff722 100644
--- a/java/com/google/gerrit/plugins/checks/CheckerQuery.java
+++ b/java/com/google/gerrit/plugins/checks/CheckerQuery.java
@@ -22,13 +22,44 @@
 import static com.google.gerrit.index.query.QueryParser.OR;
 import static java.util.Objects.requireNonNull;
 
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
+import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableSortedSet;
-import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.index.query.IndexPredicate;
+import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.index.query.QueryParser;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.ChangeQueryProcessor;
+import com.google.gerrit.server.query.change.ChangeStatusPredicate;
+import com.google.gerrit.server.query.change.ProjectPredicate;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryHelper.ActionType;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Consumer;
 import org.antlr.runtime.tree.Tree;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 
+/**
+ * Utility for validating and executing relevancy queries for checkers.
+ *
+ * <p>Instances are not threadsafe and should not be reused across requests. However, they may be
+ * reused within a single request.
+ */
 public class CheckerQuery {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   // Note that this list contains *operators*, not predicates. If there are multiple operators
   // aliased together to the same predicate ("f:", "file:"), they all need to be listed explicitly.
   //
@@ -88,18 +119,8 @@
           "unresolved",
           "wip");
 
-  /**
-   * Cleans a query string for storage in the checker configuration.
-   *
-   * <p>The query string is interpreted as a change query. Only a subset of query operators are
-   * supported, as listed in the REST API documentation and {@link #ALLOWED_OPERATORS}.
-   *
-   * @param query a change query string.
-   * @return the query string, trimmed. May be empty, which indicates all changes match.
-   * @throws BadRequestException if the query is not a valid query, or it uses operators outside of
-   *     the allowed set.
-   */
-  public static String clean(String query) throws BadRequestException {
+  @VisibleForTesting
+  public static String clean(String query) throws ConfigInvalidException {
     String trimmed = requireNonNull(query).trim();
     if (trimmed.isEmpty()) {
       return trimmed;
@@ -107,13 +128,13 @@
     try {
       checkOperators(QueryParser.parse(query));
     } catch (QueryParseException e) {
-      throw new BadRequestException("Invalid query: " + query + "\n" + e.getMessage(), e);
+      throw new ConfigInvalidException("Invalid query: " + query + "\n" + e.getMessage(), e);
     }
 
     return trimmed;
   }
 
-  private static void checkOperators(Tree node) throws BadRequestException {
+  private static void checkOperators(Tree node) throws ConfigInvalidException {
     switch (node.getType()) {
       case AND:
       case OR:
@@ -125,15 +146,16 @@
 
       case FIELD_NAME:
         if (!ALLOWED_OPERATORS.contains(node.getText())) {
-          throw new BadRequestException("Unsupported operator: " + node);
+          throw new ConfigInvalidException("Unsupported operator: " + node);
         }
         break;
 
       case DEFAULT_FIELD:
-        throw new BadRequestException("Specific search operator required: " + getOnlyChild(node));
+        throw new ConfigInvalidException(
+            "Specific search operator required: " + getOnlyChild(node));
 
       default:
-        throw new BadRequestException("Unsupported query: " + node);
+        throw new ConfigInvalidException("Unsupported query: " + node);
     }
   }
 
@@ -142,5 +164,169 @@
     return node.getChild(0);
   }
 
-  private CheckerQuery() {}
+  private final RetryHelper retryHelper;
+  private final Provider<ChangeQueryProcessor> changeQueryProcessorProvider;
+  private final ChangeQueryBuilder queryBuilder;
+
+  @Inject
+  CheckerQuery(
+      RetryHelper retryHelper,
+      Provider<AnonymousUser> anonymousUserProvider,
+      Provider<ChangeQueryBuilder> queryBuilderProvider,
+      Provider<ChangeQueryProcessor> changeQueryProcessorProvider) {
+    this.retryHelper = retryHelper;
+    this.changeQueryProcessorProvider = changeQueryProcessorProvider;
+    // The user passed to the ChangeQueryBuilder just controls how it parses "self". Anonymous means
+    // "self" is disallowed, which is correct for checker queries, since the results should not
+    // depend on the calling user. However, note that results are still filtered by visibility, but
+    // visibility is controlled by ChangeQueryProcessor, which always uses the current user and
+    // can't be overridden.
+    this.queryBuilder = queryBuilderProvider.get().asUser(anonymousUserProvider.get());
+  }
+
+  public boolean isCheckerRelevant(Checker checker, ChangeData cd) throws OrmException {
+    if (!checker.getQuery().isPresent()) {
+      return cd.change().isNew();
+    }
+
+    Predicate<ChangeData> predicate;
+    try {
+      predicate =
+          createQueryPredicate(checker.getUuid(), checker.getRepository(), checker.getQuery());
+    } catch (ConfigInvalidException e) {
+      logger.atWarning().withCause(e).log(
+          "skipping invalid query for checker %s", checker.getUuid());
+      return false;
+    }
+
+    return predicate.asMatchable().match(cd);
+  }
+
+  /**
+   * Cleans and validates a query string for storage in the checker configuration.
+   *
+   * <p>The query string is interpreted as a change query. Only a subset of query operators are
+   * supported, as listed in the REST API documentation and {@link #ALLOWED_OPERATORS}.
+   *
+   * <p>In addition to syntactic validation and checking for allowed operators, this method actually
+   * performs a query against the index, to ensure it passes any restrictions imposed by the index
+   * implementation, such as length limits.
+   *
+   * @param checkerUuid the checker UUID.
+   * @param repository the checker repository.
+   * @param query a change query string, either from the checker in storage or a proposed new value
+   *     provided by a user.
+   * @return the query string, trimmed. May be empty, which indicates all changes match.
+   * @throws ConfigInvalidException if the query is not a valid query, or it uses operators outside
+   *     of the allowed set.
+   */
+  public String validate(CheckerUuid checkerUuid, Project.NameKey repository, String query)
+      throws ConfigInvalidException, OrmException {
+    // This parses the query string twice, which is unavoidable since there is currently no
+    // QueryProcessor API which takes an Antlr Tree. That's ok; the parse cost is vastly outweighed
+    // by the actual query execution.
+    query = clean(query);
+    queryMatchingChanges(
+        checkerUuid,
+        repository,
+        Optional.ofNullable(Strings.emptyToNull(query)),
+        qp -> qp.setUserProvidedLimit(1));
+    return query;
+  }
+
+  public List<ChangeData> queryMatchingChanges(Checker checker)
+      throws ConfigInvalidException, OrmException {
+    return queryMatchingChanges(
+        checker.getUuid(), checker.getRepository(), checker.getQuery(), qp -> {});
+  }
+
+  private List<ChangeData> queryMatchingChanges(
+      CheckerUuid checkerUuid,
+      Project.NameKey repository,
+      Optional<String> optionalQuery,
+      Consumer<ChangeQueryProcessor> queryProcessorSetup)
+      throws ConfigInvalidException, OrmException {
+    try {
+      return executeIndexQueryWithRetry(
+          queryProcessorSetup, createQueryPredicate(checkerUuid, repository, optionalQuery));
+    } catch (QueryParseException e) {
+      throw invalidQueryException(checkerUuid, optionalQuery, e);
+    }
+  }
+
+  private Predicate<ChangeData> createQueryPredicate(
+      CheckerUuid checkerUuid, Project.NameKey repository, Optional<String> optionalQuery)
+      throws ConfigInvalidException {
+    Predicate<ChangeData> predicate = new ProjectPredicate(repository.get());
+
+    if (optionalQuery.isPresent()) {
+      String query = optionalQuery.get();
+      Predicate<ChangeData> predicateForQuery;
+      try {
+        predicateForQuery = queryBuilder.parse(query);
+      } catch (QueryParseException e) {
+        throw invalidQueryException(checkerUuid, optionalQuery, e);
+      }
+
+      if (!predicateForQuery.isMatchable()) {
+        // Assuming nobody modified the query behind Gerrit's back, this is programmer error:
+        // CheckerQuery should not be able to produce non-matchable queries.
+        logger.atWarning().log(
+            "change query of checker %s is not matchable: %s", checkerUuid, optionalQuery.get());
+        throw invalidQueryException(checkerUuid, optionalQuery, null);
+      }
+
+      predicate = Predicate.and(predicate, predicateForQuery);
+    }
+
+    if (!hasStatusPredicate(predicate)) {
+      predicate = Predicate.and(ChangeStatusPredicate.open(), predicate);
+    }
+
+    return predicate;
+  }
+
+  private static boolean hasStatusPredicate(Predicate<ChangeData> predicate) {
+    if (predicate instanceof IndexPredicate) {
+      return ((IndexPredicate<ChangeData>) predicate)
+          .getField()
+          .getName()
+          .equals(ChangeField.STATUS.getName());
+    }
+    return predicate.getChildren().stream().anyMatch(CheckerQuery::hasStatusPredicate);
+  }
+
+  // TODO(ekempin): Retrying the query should be done by ChangeQueryProcessor.
+  private List<ChangeData> executeIndexQueryWithRetry(
+      Consumer<ChangeQueryProcessor> queryProcessorSetup, Predicate<ChangeData> predicate)
+      throws OrmException, QueryParseException {
+    try {
+      return retryHelper.execute(
+          ActionType.INDEX_QUERY,
+          () -> {
+            ChangeQueryProcessor qp = changeQueryProcessorProvider.get();
+            queryProcessorSetup.accept(qp);
+            return qp.query(predicate).entities();
+          },
+          OrmException.class::isInstance);
+    } catch (Exception e) {
+      Throwables.throwIfUnchecked(e);
+      Throwables.throwIfInstanceOf(e, QueryParseException.class);
+      Throwables.throwIfInstanceOf(e, OrmException.class);
+      throw new OrmException(e);
+    }
+  }
+
+  private static ConfigInvalidException invalidQueryException(
+      CheckerUuid checkerUuid,
+      Optional<String> optionalQuery,
+      @Nullable QueryParseException parseException) {
+    String msg =
+        String.format(
+            "change query of checker %s is invalid: %s", checkerUuid, optionalQuery.orElse(""));
+    if (parseException != null) {
+      msg += " (" + parseException.getMessage() + ")";
+    }
+    return new ConfigInvalidException(msg, parseException);
+  }
 }
diff --git a/java/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckerTestData.java b/java/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckerTestData.java
index 44883e2..9fd05a0 100644
--- a/java/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckerTestData.java
+++ b/java/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckerTestData.java
@@ -14,6 +14,10 @@
 
 package com.google.gerrit.plugins.checks.acceptance.testsuite;
 
+import static java.util.stream.Collectors.joining;
+
+import java.util.stream.Stream;
+
 public final class CheckerTestData {
   /** An invalid checker UUID. */
   public static final String INVALID_UUID = "notauuid";
@@ -38,5 +42,10 @@
   /** Query that is not parsable. */
   public static final String INVALID_QUERY = ":foo :bar";
 
+  /** Query with {@code numTerms} total operators, all of which are supported in checker queries. */
+  public static String longQueryWithSupportedOperators(int numTerms) {
+    return Stream.generate(() -> "file:foo").limit(numTerms).collect(joining(" "));
+  }
+
   private CheckerTestData() {}
 }
diff --git a/java/com/google/gerrit/plugins/checks/api/CreateChecker.java b/java/com/google/gerrit/plugins/checks/api/CreateChecker.java
index 7bf61af..a935f14 100644
--- a/java/com/google/gerrit/plugins/checks/api/CreateChecker.java
+++ b/java/com/google/gerrit/plugins/checks/api/CreateChecker.java
@@ -42,6 +42,7 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gwtorm.server.OrmDuplicateKeyException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -53,6 +54,7 @@
     implements RestCollectionModifyView<TopLevelResource, CheckerResource, CheckerInput> {
   private final Provider<CurrentUser> self;
   private final PermissionBackend permissionBackend;
+  private final Provider<CheckerQuery> checkerQueryProvider;
   private final Provider<CheckersUpdate> checkersUpdate;
   private final CheckerJson checkerJson;
   private final AdministrateCheckersPermission permission;
@@ -62,12 +64,14 @@
   public CreateChecker(
       Provider<CurrentUser> self,
       PermissionBackend permissionBackend,
+      Provider<CheckerQuery> checkerQueryProvider,
       @UserInitiated Provider<CheckersUpdate> checkersUpdate,
       CheckerJson checkerJson,
       AdministrateCheckersPermission permission,
       ProjectCache projectCache) {
     this.self = self;
     this.permissionBackend = permissionBackend;
+    this.checkerQueryProvider = checkerQueryProvider;
     this.checkersUpdate = checkersUpdate;
     this.checkerJson = checkerJson;
     this.permission = permission;
@@ -76,7 +80,8 @@
 
   @Override
   public Response<CheckerInfo> apply(TopLevelResource parentResource, CheckerInput input)
-      throws RestApiException, PermissionBackendException, IOException, ConfigInvalidException {
+      throws RestApiException, PermissionBackendException, IOException, ConfigInvalidException,
+          OrmException {
     if (!self.get().isIdentifiedUser()) {
       throw new AuthException("Authentication required");
     }
@@ -116,7 +121,7 @@
       checkerUpdateBuilder.setBlockingConditions(ImmutableSortedSet.copyOf(input.blocking));
     }
     if (input.query != null) {
-      checkerUpdateBuilder.setQuery(CheckerQuery.clean(input.query));
+      checkerUpdateBuilder.setQuery(validateQuery(checkerUuid, repository, input.query));
     }
     try {
       Checker checker =
@@ -142,4 +147,13 @@
 
     return projectState.getNameKey();
   }
+
+  private String validateQuery(CheckerUuid checkerUuid, Project.NameKey repository, String query)
+      throws BadRequestException, OrmException {
+    try {
+      return checkerQueryProvider.get().validate(checkerUuid, repository, query);
+    } catch (ConfigInvalidException e) {
+      throw new BadRequestException(e.getMessage(), e);
+    }
+  }
 }
diff --git a/java/com/google/gerrit/plugins/checks/api/QueryPendingChecks.java b/java/com/google/gerrit/plugins/checks/api/QueryPendingChecks.java
index cededfa..fb7a03a 100644
--- a/java/com/google/gerrit/plugins/checks/api/QueryPendingChecks.java
+++ b/java/com/google/gerrit/plugins/checks/api/QueryPendingChecks.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.plugins.checks.Check;
 import com.google.gerrit.plugins.checks.CheckKey;
 import com.google.gerrit.plugins.checks.Checker;
+import com.google.gerrit.plugins.checks.CheckerQuery;
 import com.google.gerrit.plugins.checks.CheckerUuid;
 import com.google.gerrit.plugins.checks.Checkers;
 import com.google.gerrit.plugins.checks.Checks;
@@ -38,9 +39,6 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.query.change.ChangeQueryBuilder;
-import com.google.gerrit.server.query.change.ChangeQueryProcessor;
-import com.google.gerrit.server.update.RetryHelper;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -55,9 +53,7 @@
   private final CheckQueryBuilder checkQueryBuilder;
   private final Checkers checkers;
   private final Checks checks;
-  private final RetryHelper retryHelper;
-  private final Provider<ChangeQueryBuilder> queryBuilderProvider;
-  private final Provider<ChangeQueryProcessor> changeQueryProcessorProvider;
+  private final Provider<CheckerQuery> checkerQueryProvider;
 
   private String queryString;
 
@@ -76,19 +72,16 @@
       CheckQueryBuilder checkQueryBuilder,
       Checkers checkers,
       Checks checks,
-      RetryHelper retryHelper,
-      Provider<ChangeQueryBuilder> queryBuilderProvider,
-      Provider<ChangeQueryProcessor> changeQueryProcessorProvider) {
+      Provider<CheckerQuery> checkerQueryProvider) {
     this.checkQueryBuilder = checkQueryBuilder;
     this.checkers = checkers;
     this.checks = checks;
-    this.retryHelper = retryHelper;
-    this.queryBuilderProvider = queryBuilderProvider;
-    this.changeQueryProcessorProvider = changeQueryProcessorProvider;
+    this.checkerQueryProvider = checkerQueryProvider;
   }
 
   public List<PendingChecksInfo> apply()
-      throws RestApiException, IOException, ConfigInvalidException, OrmException {
+      throws RestApiException, IOException, ConfigInvalidException, OrmException,
+          QueryParseException {
     return apply(TopLevelResource.INSTANCE);
   }
 
@@ -111,11 +104,7 @@
 
     // The query system can only match against the current patch set; ignore non-current patch sets
     // for now.
-    List<ChangeData> changes =
-        checker
-            .get()
-            .queryMatchingChanges(
-                retryHelper, queryBuilderProvider.get(), changeQueryProcessorProvider);
+    List<ChangeData> changes = checkerQueryProvider.get().queryMatchingChanges(checker.get());
     CheckerUuid checkerUuid = checker.get().getUuid();
     List<PendingChecksInfo> pendingChecks = new ArrayList<>(changes.size());
     for (ChangeData cd : changes) {
@@ -158,7 +147,7 @@
       // if the root predicate is an AndPredicate, any of its direct children must be a
       // CheckerPredicate, the other child predicates can be anything (including any combination of
       // AndPredicate, OrPredicate and NotPredicate).
-      if (!predicate.getChildren().stream().anyMatch(CheckerPredicate.class::isInstance)) {
+      if (predicate.getChildren().stream().noneMatch(CheckerPredicate.class::isInstance)) {
         throw new BadRequestException(
             String.format(
                 "query must be '%s:<checker-uuid>' or '%s:<checker-uuid> AND <other-operators>'",
diff --git a/java/com/google/gerrit/plugins/checks/api/UpdateChecker.java b/java/com/google/gerrit/plugins/checks/api/UpdateChecker.java
index 362fde8..9c3158b 100644
--- a/java/com/google/gerrit/plugins/checks/api/UpdateChecker.java
+++ b/java/com/google/gerrit/plugins/checks/api/UpdateChecker.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.plugins.checks.CheckerName;
 import com.google.gerrit.plugins.checks.CheckerQuery;
 import com.google.gerrit.plugins.checks.CheckerUpdate;
+import com.google.gerrit.plugins.checks.CheckerUuid;
 import com.google.gerrit.plugins.checks.CheckersUpdate;
 import com.google.gerrit.plugins.checks.NoSuchCheckerException;
 import com.google.gerrit.plugins.checks.UrlValidator;
@@ -35,6 +36,7 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -43,38 +45,41 @@
 
 @Singleton
 public class UpdateChecker implements RestModifyView<CheckerResource, CheckerInput> {
-  private final PermissionBackend permissionBackend;
-  private final Provider<CheckersUpdate> checkersUpdate;
   private final CheckerJson checkerJson;
+  private final PermissionBackend permissionBackend;
   private final ProjectCache projectCache;
-
+  private final Provider<CheckerQuery> checkerQueryProvider;
+  private final Provider<CheckersUpdate> checkersUpdate;
   private final AdministrateCheckersPermission permission;
 
   @Inject
   public UpdateChecker(
-      PermissionBackend permissionBackend,
-      @UserInitiated Provider<CheckersUpdate> checkersUpdate,
       CheckerJson checkerJson,
-      AdministrateCheckersPermission permission,
-      ProjectCache projectCache) {
-    this.permissionBackend = permissionBackend;
-    this.checkersUpdate = checkersUpdate;
+      ProjectCache projectCache,
+      PermissionBackend permissionBackend,
+      Provider<CheckerQuery> checkerQueryProvider,
+      @UserInitiated Provider<CheckersUpdate> checkersUpdate,
+      AdministrateCheckersPermission permission) {
     this.checkerJson = checkerJson;
-    this.permission = permission;
     this.projectCache = projectCache;
+    this.permissionBackend = permissionBackend;
+    this.checkerQueryProvider = checkerQueryProvider;
+    this.checkersUpdate = checkersUpdate;
+    this.permission = permission;
   }
 
   @Override
   public CheckerInfo apply(CheckerResource resource, CheckerInput input)
       throws RestApiException, PermissionBackendException, NoSuchCheckerException, IOException,
-          ConfigInvalidException {
+          ConfigInvalidException, OrmException {
     permissionBackend.currentUser().check(permission);
 
+    CheckerUuid checkerUuid = resource.getChecker().getUuid();
     CheckerUpdate.Builder checkerUpdateBuilder = CheckerUpdate.builder();
 
     // Callers shouldn't really be providing UUID. If they do, the only legal UUID is exactly the
     // current UUID.
-    if (input.uuid != null && !input.uuid.equals(resource.getChecker().getUuid().get())) {
+    if (input.uuid != null && !input.uuid.equals(checkerUuid.get())) {
       throw new BadRequestException("uuid cannot be updated");
     }
 
@@ -94,9 +99,12 @@
       checkerUpdateBuilder.setUrl(UrlValidator.clean(input.url));
     }
 
+    Project.NameKey repository;
     if (input.repository != null) {
-      Project.NameKey repository = resolveRepository(input.repository);
+      repository = resolveRepository(input.repository);
       checkerUpdateBuilder.setRepository(repository);
+    } else {
+      repository = resource.getChecker().getRepository();
     }
 
     if (input.status != null) {
@@ -108,13 +116,11 @@
     }
 
     if (input.query != null) {
-      checkerUpdateBuilder.setQuery(CheckerQuery.clean(input.query));
+      checkerUpdateBuilder.setQuery(validateQuery(checkerUuid, repository, input.query));
     }
 
     Checker updatedChecker =
-        checkersUpdate
-            .get()
-            .updateChecker(resource.getChecker().getUuid(), checkerUpdateBuilder.build());
+        checkersUpdate.get().updateChecker(checkerUuid, checkerUpdateBuilder.build());
     return checkerJson.format(updatedChecker);
   }
 
@@ -131,4 +137,13 @@
 
     return projectState.getNameKey();
   }
+
+  private String validateQuery(CheckerUuid checkerUuid, Project.NameKey repository, String query)
+      throws BadRequestException, OrmException {
+    try {
+      return checkerQueryProvider.get().validate(checkerUuid, repository, query);
+    } catch (ConfigInvalidException e) {
+      throw new BadRequestException(e.getMessage(), e);
+    }
+  }
 }
diff --git a/java/com/google/gerrit/plugins/checks/db/CheckBackfiller.java b/java/com/google/gerrit/plugins/checks/db/CheckBackfiller.java
index 2b1a26a..41d3592 100644
--- a/java/com/google/gerrit/plugins/checks/db/CheckBackfiller.java
+++ b/java/com/google/gerrit/plugins/checks/db/CheckBackfiller.java
@@ -17,13 +17,12 @@
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.plugins.checks.Check;
 import com.google.gerrit.plugins.checks.Checker;
+import com.google.gerrit.plugins.checks.CheckerQuery;
 import com.google.gerrit.plugins.checks.CheckerUuid;
 import com.google.gerrit.plugins.checks.Checkers;
 import com.google.gerrit.plugins.checks.api.CheckerStatus;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -36,17 +35,12 @@
 @Singleton
 class CheckBackfiller {
   private final Checkers checkers;
-  private final Provider<AnonymousUser> anonymousUserProvider;
-  private final Provider<ChangeQueryBuilder> queryBuilderProvider;
+  private final Provider<CheckerQuery> checkerQueryProvider;
 
   @Inject
-  CheckBackfiller(
-      Checkers checkers,
-      Provider<AnonymousUser> anonymousUserProvider,
-      Provider<ChangeQueryBuilder> queryBuilderProvider) {
+  CheckBackfiller(Checkers checkers, Provider<CheckerQuery> checkerQueryProvider) {
     this.checkers = checkers;
-    this.anonymousUserProvider = anonymousUserProvider;
-    this.queryBuilderProvider = queryBuilderProvider;
+    this.checkerQueryProvider = checkerQueryProvider;
   }
 
   ImmutableList<Check> getBackfilledChecksForRelevantCheckers(
@@ -61,11 +55,11 @@
     }
     // All candidates need to be checked for relevance. Any relevant checkers are reported as
     // NOT_STARTED, with creation time matching the patch set.
+    CheckerQuery checkerQuery = checkerQueryProvider.get();
     ImmutableList.Builder<Check> result = ImmutableList.builderWithExpectedSize(candidates.size());
     PatchSet ps = cd.patchSet(psId);
-    ChangeQueryBuilder queryBuilder = newQueryBuilder();
     for (Checker checker : candidates) {
-      if (checker.isCheckerRelevant(cd, queryBuilder)) {
+      if (checkerQuery.isCheckerRelevant(checker, cd)) {
         // Add synthetic check at the creation time of the patch set.
         result.add(Check.newBackfilledCheck(cd.project(), ps, checker));
       }
@@ -90,13 +84,9 @@
     }
     if (!checker.isPresent()
         || checker.get().getStatus() != CheckerStatus.ENABLED
-        || !checker.get().isCheckerRelevant(cd, newQueryBuilder())) {
+        || !checkerQueryProvider.get().isCheckerRelevant(checker.get(), cd)) {
       return Optional.empty();
     }
     return Optional.of(Check.newBackfilledCheck(cd.project(), cd.patchSet(psId), checker.get()));
   }
-
-  private ChangeQueryBuilder newQueryBuilder() {
-    return queryBuilderProvider.get().asUser(anonymousUserProvider.get());
-  }
 }
diff --git a/java/com/google/gerrit/plugins/checks/db/NoteDbChecks.java b/java/com/google/gerrit/plugins/checks/db/NoteDbChecks.java
index c73a0f1..8a3f2fd 100644
--- a/java/com/google/gerrit/plugins/checks/db/NoteDbChecks.java
+++ b/java/com/google/gerrit/plugins/checks/db/NoteDbChecks.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.plugins.checks.Check;
 import com.google.gerrit.plugins.checks.CheckKey;
 import com.google.gerrit.plugins.checks.Checker;
+import com.google.gerrit.plugins.checks.CheckerQuery;
 import com.google.gerrit.plugins.checks.CheckerUuid;
 import com.google.gerrit.plugins.checks.Checkers;
 import com.google.gerrit.plugins.checks.Checks;
@@ -34,9 +35,7 @@
 import com.google.gerrit.reviewdb.client.PatchSet.Id;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.Project.NameKey;
-import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -54,8 +53,7 @@
   private final CheckNotes.Factory checkNotesFactory;
   private final Checkers checkers;
   private final CheckBackfiller checkBackfiller;
-  private final Provider<AnonymousUser> anonymousUserProvider;
-  private final Provider<ChangeQueryBuilder> queryBuilderProvider;
+  private final Provider<CheckerQuery> checkerQueryProvider;
 
   @Inject
   NoteDbChecks(
@@ -63,14 +61,12 @@
       CheckNotes.Factory checkNotesFactory,
       Checkers checkers,
       CheckBackfiller checkBackfiller,
-      Provider<AnonymousUser> anonymousUserProvider,
-      Provider<ChangeQueryBuilder> queryBuilderProvider) {
+      Provider<CheckerQuery> checkerQueryProvider) {
     this.changeDataFactory = changeDataFactory;
     this.checkNotesFactory = checkNotesFactory;
     this.checkers = checkers;
     this.checkBackfiller = checkBackfiller;
-    this.anonymousUserProvider = anonymousUserProvider;
-    this.queryBuilderProvider = queryBuilderProvider;
+    this.checkerQueryProvider = checkerQueryProvider;
   }
 
   @Override
@@ -133,6 +129,7 @@
   public CombinedCheckState getCombinedCheckState(NameKey projectName, Id patchSetId)
       throws IOException, OrmException {
     ChangeData changeData = changeDataFactory.create(projectName, patchSetId.changeId);
+    CheckerQuery checkerQuery = checkerQueryProvider.get();
     ImmutableMap<String, Checker> allCheckersOfProject =
         checkers.checkersOf(projectName).stream()
             .collect(ImmutableMap.toImmutableMap(c -> c.getUuid().get(), c -> c));
@@ -143,8 +140,6 @@
         getChecks(projectName, patchSetId, GetCheckOptions.withBackfilling()).stream()
             .collect(ImmutableMap.toImmutableMap(c -> c.key().checkerUuid().get(), c -> c));
 
-    ChangeQueryBuilder queryBuilder =
-        queryBuilderProvider.get().asUser(anonymousUserProvider.get());
     ImmutableListMultimap.Builder<CheckState, Boolean> statesAndRequired =
         ImmutableListMultimap.builder();
 
@@ -162,7 +157,7 @@
       boolean isRequired =
           checker.getStatus() == CheckerStatus.ENABLED
               && checker.isRequired()
-              && checker.isCheckerRelevant(changeData, queryBuilder);
+              && checkerQuery.isCheckerRelevant(checker, changeData);
       statesAndRequired.put(check.state(), isRequired);
     }
 
diff --git a/javatests/com/google/gerrit/plugins/checks/CheckerQueryTest.java b/javatests/com/google/gerrit/plugins/checks/CheckerQueryTest.java
index 41ce8ee..2ba3061 100644
--- a/javatests/com/google/gerrit/plugins/checks/CheckerQueryTest.java
+++ b/javatests/com/google/gerrit/plugins/checks/CheckerQueryTest.java
@@ -17,12 +17,12 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assert_;
 
-import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.index.query.QueryParseException;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.gerrit.testing.GerritBaseTests;
 import com.google.gerrit.testing.InMemoryModule;
 import com.google.inject.Guice;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.junit.Test;
 
 public class CheckerQueryTest extends GerritBaseTests {
@@ -107,8 +107,8 @@
   private static void assertInvalidQuery(String query, String expectedMessage) {
     try {
       CheckerQuery.clean(query);
-      assert_().fail("expected BadRequestException");
-    } catch (BadRequestException e) {
+      assert_().fail("expected ConfigInvalidException");
+    } catch (ConfigInvalidException e) {
       assertThat(e).hasMessageThat().isEqualTo(expectedMessage);
     }
   }
@@ -116,7 +116,7 @@
   private static void assertValidQuery(String query) {
     try {
       assertThat(CheckerQuery.clean(query)).isEqualTo(query.trim());
-    } catch (BadRequestException e) {
+    } catch (ConfigInvalidException e) {
       throw new AssertionError("expected valid query: " + query, e);
     }
   }
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/api/CreateCheckerIT.java b/javatests/com/google/gerrit/plugins/checks/acceptance/api/CreateCheckerIT.java
index 1733f57..3a69967 100644
--- a/javatests/com/google/gerrit/plugins/checks/acceptance/api/CreateCheckerIT.java
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/api/CreateCheckerIT.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.plugins.checks.CheckerQuery;
 import com.google.gerrit.plugins.checks.CheckerUuid;
 import com.google.gerrit.plugins.checks.acceptance.AbstractCheckersTest;
 import com.google.gerrit.plugins.checks.acceptance.testsuite.CheckerOperations.PerCheckerOperations;
@@ -35,19 +36,30 @@
 import com.google.gerrit.plugins.checks.api.CheckerStatus;
 import com.google.gerrit.plugins.checks.db.CheckersByRepositoryNotes;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.testing.ConfigSuite;
 import com.google.gerrit.testing.TestTimeUtil;
 import com.google.inject.Inject;
 import java.sql.Timestamp;
 import java.time.Instant;
 import java.util.concurrent.TimeUnit;
+import org.eclipse.jgit.lib.Config;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
 public class CreateCheckerIT extends AbstractCheckersTest {
+  private static final int MAX_INDEX_TERMS = 10;
+
   @Inject private RequestScopeOperations requestScopeOperations;
   @Inject private ProjectOperations projectOperations;
 
+  @ConfigSuite.Default
+  public static Config defaultConfig() {
+    Config cfg = new Config();
+    cfg.setInt("index", null, "maxTerms", MAX_INDEX_TERMS);
+    return cfg;
+  }
+
   @Before
   public void setTimeForTesting() {
     TestTimeUtil.resetWithClockStep(1, TimeUnit.SECONDS);
@@ -398,6 +410,28 @@
   }
 
   @Test
+  public void createCheckerWithTooLongQueryFails() throws Exception {
+    CheckerInput input = new CheckerInput();
+    input.uuid = "test:my-checker";
+    input.repository = allProjects.get();
+    input.query = CheckerTestData.longQueryWithSupportedOperators(MAX_INDEX_TERMS * 2);
+    assertThat(CheckerQuery.clean(input.query)).isEqualTo(input.query);
+    try {
+      checkersApi.create(input).get();
+      assert_().fail("expected BadRequestException");
+    } catch (BadRequestException e) {
+      assertThat(e)
+          .hasMessageThat()
+          .isEqualTo(
+              "change query of checker "
+                  + input.uuid
+                  + " is invalid: "
+                  + input.query
+                  + " (too many terms in query)");
+    }
+  }
+
+  @Test
   public void createMultipleCheckers() throws Exception {
     Project.NameKey repositoryName1 = projectOperations.newProject().create();
     Project.NameKey repositoryName2 = projectOperations.newProject().create();
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/api/UpdateCheckerIT.java b/javatests/com/google/gerrit/plugins/checks/acceptance/api/UpdateCheckerIT.java
index a0f2663..2809f02 100644
--- a/javatests/com/google/gerrit/plugins/checks/acceptance/api/UpdateCheckerIT.java
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/api/UpdateCheckerIT.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.plugins.checks.Checker;
+import com.google.gerrit.plugins.checks.CheckerQuery;
 import com.google.gerrit.plugins.checks.CheckerUuid;
 import com.google.gerrit.plugins.checks.acceptance.AbstractCheckersTest;
 import com.google.gerrit.plugins.checks.acceptance.testsuite.CheckerOperations.PerCheckerOperations;
@@ -51,16 +52,18 @@
 
 @SkipProjectClone
 public class UpdateCheckerIT extends AbstractCheckersTest {
-  @Inject private RequestScopeOperations requestScopeOperations;
-  @Inject private ProjectOperations projectOperations;
+  private static final int MAX_INDEX_TERMS = 10;
 
   @ConfigSuite.Default
   public static Config defaultConfig() {
     Config cfg = new Config();
-    cfg.setBoolean("checks", "api", "enabled", true);
+    cfg.setInt("index", null, "maxTerms", MAX_INDEX_TERMS);
     return cfg;
   }
 
+  @Inject private RequestScopeOperations requestScopeOperations;
+  @Inject private ProjectOperations projectOperations;
+
   @Before
   public void setTimeForTesting() {
     TestTimeUtil.resetWithClockStep(1, TimeUnit.SECONDS);
@@ -516,6 +519,31 @@
   }
 
   @Test
+  public void updateWithTooLongQuery() throws Exception {
+    CheckerUuid checkerUuid = checkerOperations.newChecker().create();
+    Optional<String> oldQuery = checkerOperations.checker(checkerUuid).get().getQuery();
+
+    CheckerInput input = new CheckerInput();
+    input.query = CheckerTestData.longQueryWithSupportedOperators(MAX_INDEX_TERMS * 2);
+    assertThat(CheckerQuery.clean(input.query)).isEqualTo(input.query);
+    try {
+      checkersApi.id(checkerUuid).update(input);
+      assert_().fail("expected BadRequestException");
+    } catch (BadRequestException e) {
+      assertThat(e)
+          .hasMessageThat()
+          .isEqualTo(
+              "change query of checker "
+                  + checkerUuid
+                  + " is invalid: "
+                  + input.query
+                  + " (too many terms in query)");
+    }
+
+    assertThat(checkerOperations.checker(checkerUuid).get().getQuery()).isEqualTo(oldQuery);
+  }
+
+  @Test
   public void updateResultsInNewUpdatedTimestamp() throws Exception {
     CheckerUuid checkerUuid = checkerOperations.newChecker().create();
 
diff --git a/javatests/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckerTestDataTest.java b/javatests/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckerTestDataTest.java
index f4761fe..37b22e0 100644
--- a/javatests/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckerTestDataTest.java
+++ b/javatests/com/google/gerrit/plugins/checks/acceptance/testsuite/CheckerTestDataTest.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.plugins.checks.CheckerUuid;
 import com.google.gerrit.plugins.checks.UrlValidator;
 import com.google.gerrit.plugins.checks.acceptance.AbstractCheckersTest;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.junit.Test;
 
 public class CheckerTestDataTest extends AbstractCheckersTest {
@@ -49,11 +50,18 @@
     assertInvalidQuery(CheckerTestData.INVALID_QUERY, "invalid", CheckerTestData.INVALID_QUERY);
   }
 
+  @Test
+  public void verifyTooLongQuery() throws Exception {
+    String query = CheckerTestData.longQueryWithSupportedOperators(5);
+    assertThat(query).isEqualTo("file:foo file:foo file:foo file:foo file:foo");
+    CheckerQuery.clean(query);
+  }
+
   private static void assertInvalidQuery(String query, String... expectedMessageParts) {
     try {
       CheckerQuery.clean(query);
-      assert_().fail("expected BadRequestException");
-    } catch (BadRequestException e) {
+      assert_().fail("expected ConfigInvalidException");
+    } catch (ConfigInvalidException e) {
       assertMessage(e, expectedMessageParts);
     }
   }
diff --git a/javatests/com/google/gerrit/plugins/checks/db/CheckerConfigTest.java b/javatests/com/google/gerrit/plugins/checks/db/CheckerConfigTest.java
index 05ba663..dece6af 100644
--- a/javatests/com/google/gerrit/plugins/checks/db/CheckerConfigTest.java
+++ b/javatests/com/google/gerrit/plugins/checks/db/CheckerConfigTest.java
@@ -21,7 +21,6 @@
 
 import com.google.common.collect.ImmutableSortedSet;
 import com.google.common.truth.StringSubject;
-import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.plugins.checks.CheckerCreation;
 import com.google.gerrit.plugins.checks.CheckerQuery;
 import com.google.gerrit.plugins.checks.CheckerUpdate;
@@ -461,8 +460,8 @@
     String query = "foo:bar";
     try {
       CheckerQuery.clean(query);
-      assert_().fail("expected BadRequestException");
-    } catch (BadRequestException e) {
+      assert_().fail("expected ConfigInvalidException");
+    } catch (ConfigInvalidException e) {
       // Expected.
     }