CheckerQuery: Handle QueryParseException from query execution

QueryParseException may be thrown not just while parsing the query, but
also from ChangeQueryProcessor#query. Ensure this exception is converted
to ConfigInvalidException rather than wrapped in an opaque OrmException.

Also factor out a method to use consistent messages in
ConfigInvalidExceptions thrown from CheckerQuery.

Change-Id: Idd08b98dde7b26e82f644a6c3d88b3cfd9af0555
diff --git a/java/com/google/gerrit/plugins/checks/CheckerQuery.java b/java/com/google/gerrit/plugins/checks/CheckerQuery.java
index 0830d8b..8a9dc84 100644
--- a/java/com/google/gerrit/plugins/checks/CheckerQuery.java
+++ b/java/com/google/gerrit/plugins/checks/CheckerQuery.java
@@ -25,6 +25,7 @@
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableSortedSet;
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.index.query.IndexPredicate;
 import com.google.gerrit.index.query.Predicate;
@@ -207,7 +208,11 @@
 
   public List<ChangeData> queryMatchingChanges(Checker checker)
       throws ConfigInvalidException, OrmException {
-    return executeIndexQueryWithRetry(createQueryPredicate(checker));
+    try {
+      return executeIndexQueryWithRetry(createQueryPredicate(checker));
+    } catch (QueryParseException e) {
+      throw invalidQueryException(checker, e);
+    }
   }
 
   private Predicate<ChangeData> createQueryPredicate(Checker checker)
@@ -220,17 +225,16 @@
       try {
         predicateForQuery = queryBuilder.parse(query);
       } catch (QueryParseException e) {
-        throw new ConfigInvalidException(
-            String.format("change query of checker %s is invalid: %s", checker.getUuid(), query),
-            e);
+        throw invalidQueryException(checker, 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", checker.getUuid(), query));
+        logger.atWarning().log(
+            "change query of checker %s is not matchable: %s",
+            checker.getUuid(), checker.getQuery().get());
+        throw invalidQueryException(checker, null);
       }
 
       predicate = Predicate.and(predicate, predicateForQuery);
@@ -255,7 +259,7 @@
 
   // TODO(ekempin): Retrying the query should be done by ChangeQueryProcessor.
   private List<ChangeData> executeIndexQueryWithRetry(Predicate<ChangeData> predicate)
-      throws OrmException {
+      throws OrmException, QueryParseException {
     try {
       return retryHelper.execute(
           ActionType.INDEX_QUERY,
@@ -263,8 +267,21 @@
           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(
+      Checker checker, @Nullable QueryParseException parseException) {
+    String msg =
+        String.format(
+            "change query of checker %s is invalid: %s",
+            checker.getUuid(), checker.getQuery().orElse(""));
+    if (parseException != null) {
+      msg += " (" + parseException.getMessage() + ")";
+    }
+    return new ConfigInvalidException(msg, parseException);
+  }
 }
diff --git a/java/com/google/gerrit/plugins/checks/api/QueryPendingChecks.java b/java/com/google/gerrit/plugins/checks/api/QueryPendingChecks.java
index 57f7be0..e880244 100644
--- a/java/com/google/gerrit/plugins/checks/api/QueryPendingChecks.java
+++ b/java/com/google/gerrit/plugins/checks/api/QueryPendingChecks.java
@@ -80,7 +80,8 @@
   }
 
   public List<PendingChecksInfo> apply()
-      throws RestApiException, IOException, ConfigInvalidException, OrmException {
+      throws RestApiException, IOException, ConfigInvalidException, OrmException,
+          QueryParseException {
     return apply(TopLevelResource.INSTANCE);
   }