LCOV - code coverage report
Current view: top level - server/index/change - ChangeIndexRewriter.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 134 141 95.0 %
Date: 2022-11-19 15:00:39 Functions: 16 16 100.0 %

          Line data    Source code
       1             : // Copyright (C) 2013 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.server.index.change;
      16             : 
      17             : import static com.google.gerrit.server.query.change.ChangeStatusPredicate.closed;
      18             : import static com.google.gerrit.server.query.change.ChangeStatusPredicate.open;
      19             : 
      20             : import com.google.common.collect.Lists;
      21             : import com.google.common.collect.Sets;
      22             : import com.google.gerrit.common.Nullable;
      23             : import com.google.gerrit.entities.Change;
      24             : import com.google.gerrit.entities.Change.Status;
      25             : import com.google.gerrit.index.IndexConfig;
      26             : import com.google.gerrit.index.IndexRewriter;
      27             : import com.google.gerrit.index.QueryOptions;
      28             : import com.google.gerrit.index.Schema;
      29             : import com.google.gerrit.index.SchemaFieldDefs.SchemaField;
      30             : import com.google.gerrit.index.query.AndCardinalPredicate;
      31             : import com.google.gerrit.index.query.AndPredicate;
      32             : import com.google.gerrit.index.query.HasCardinality;
      33             : import com.google.gerrit.index.query.IndexPredicate;
      34             : import com.google.gerrit.index.query.LimitPredicate;
      35             : import com.google.gerrit.index.query.NotPredicate;
      36             : import com.google.gerrit.index.query.OrCardinalPredicate;
      37             : import com.google.gerrit.index.query.OrPredicate;
      38             : import com.google.gerrit.index.query.Predicate;
      39             : import com.google.gerrit.index.query.QueryParseException;
      40             : import com.google.gerrit.index.query.TooManyTermsInQueryException;
      41             : import com.google.gerrit.server.query.change.AndChangeSource;
      42             : import com.google.gerrit.server.query.change.ChangeData;
      43             : import com.google.gerrit.server.query.change.ChangeDataSource;
      44             : import com.google.gerrit.server.query.change.ChangeQueryBuilder;
      45             : import com.google.gerrit.server.query.change.ChangeStatusPredicate;
      46             : import com.google.gerrit.server.query.change.IsSubmittablePredicate;
      47             : import com.google.gerrit.server.query.change.OrSource;
      48             : import com.google.inject.Inject;
      49             : import com.google.inject.Singleton;
      50             : import java.util.BitSet;
      51             : import java.util.EnumSet;
      52             : import java.util.List;
      53             : import java.util.Optional;
      54             : import java.util.Set;
      55             : import org.eclipse.jgit.util.MutableInteger;
      56             : 
      57             : /** Rewriter that pushes boolean logic into the secondary index. */
      58             : @Singleton
      59             : public class ChangeIndexRewriter implements IndexRewriter<ChangeData> {
      60             :   /** Set of all open change statuses. */
      61             :   public static final Set<Change.Status> OPEN_STATUSES;
      62             : 
      63             :   /** Set of all closed change statuses. */
      64             :   public static final Set<Change.Status> CLOSED_STATUSES;
      65             : 
      66             :   static {
      67         149 :     EnumSet<Change.Status> open = EnumSet.noneOf(Change.Status.class);
      68         149 :     EnumSet<Change.Status> closed = EnumSet.noneOf(Change.Status.class);
      69         149 :     for (Change.Status s : Change.Status.values()) {
      70         149 :       if (s.isOpen()) {
      71         149 :         open.add(s);
      72             :       } else {
      73         149 :         closed.add(s);
      74             :       }
      75             :     }
      76         149 :     OPEN_STATUSES = Sets.immutableEnumSet(open);
      77         149 :     CLOSED_STATUSES = Sets.immutableEnumSet(closed);
      78         149 :   }
      79             : 
      80             :   /**
      81             :    * Get the set of statuses that changes matching the given predicate may have.
      82             :    *
      83             :    * @param in predicate
      84             :    * @return the maximal set of statuses that any changes matching the input predicates may have,
      85             :    *     based on examining boolean and {@link ChangeStatusPredicate}s.
      86             :    */
      87             :   public static Set<Change.Status> getPossibleStatus(Predicate<ChangeData> in) {
      88           5 :     EnumSet<Change.Status> s = extractStatus(in);
      89           5 :     return s != null ? s : EnumSet.allOf(Change.Status.class);
      90             :   }
      91             : 
      92             :   private static @Nullable EnumSet<Change.Status> extractStatus(Predicate<ChangeData> in) {
      93           5 :     if (in instanceof ChangeStatusPredicate) {
      94           4 :       Status status = ((ChangeStatusPredicate) in).getStatus();
      95           4 :       return status != null ? EnumSet.of(status) : null;
      96           5 :     } else if (in instanceof NotPredicate) {
      97           2 :       EnumSet<Status> s = extractStatus(in.getChild(0));
      98           2 :       return s != null ? EnumSet.complementOf(s) : null;
      99           5 :     } else if (in instanceof OrPredicate) {
     100           3 :       EnumSet<Change.Status> r = null;
     101           3 :       int childrenWithStatus = 0;
     102           3 :       for (int i = 0; i < in.getChildCount(); i++) {
     103           3 :         EnumSet<Status> c = extractStatus(in.getChild(i));
     104           3 :         if (c != null) {
     105           3 :           if (r == null) {
     106           3 :             r = EnumSet.noneOf(Change.Status.class);
     107             :           }
     108           3 :           r.addAll(c);
     109           3 :           childrenWithStatus++;
     110             :         }
     111             :       }
     112           3 :       if (r != null && childrenWithStatus < in.getChildCount()) {
     113             :         // At least one child supplied a status but another did not.
     114             :         // Assume all statuses for the children that did not feed a
     115             :         // status at this part of the tree. This matches behavior if
     116             :         // the child was used at the root of a query.
     117           0 :         return EnumSet.allOf(Change.Status.class);
     118             :       }
     119           3 :       return r;
     120           5 :     } else if (in instanceof AndPredicate) {
     121           5 :       EnumSet<Change.Status> r = null;
     122           5 :       for (int i = 0; i < in.getChildCount(); i++) {
     123           5 :         EnumSet<Change.Status> c = extractStatus(in.getChild(i));
     124           5 :         if (c != null) {
     125           4 :           if (r == null) {
     126           4 :             r = EnumSet.allOf(Change.Status.class);
     127             :           }
     128           4 :           r.retainAll(c);
     129             :         }
     130             :       }
     131           5 :       return r;
     132             :     }
     133           5 :     return null;
     134             :   }
     135             : 
     136             :   private final ChangeIndexCollection indexes;
     137             :   private final IndexConfig config;
     138             : 
     139             :   @Inject
     140         149 :   ChangeIndexRewriter(ChangeIndexCollection indexes, IndexConfig config) {
     141         149 :     this.indexes = indexes;
     142         149 :     this.config = config;
     143         149 :   }
     144             : 
     145             :   @Override
     146             :   public Predicate<ChangeData> rewrite(Predicate<ChangeData> in, QueryOptions opts)
     147             :       throws QueryParseException {
     148         111 :     Predicate<ChangeData> s = rewriteImpl(in, opts);
     149         111 :     if (!(s instanceof ChangeDataSource)) {
     150           7 :       in = Predicate.and(Predicate.or(open(), closed()), in);
     151           7 :       s = rewriteImpl(in, opts);
     152             :     }
     153         111 :     if (!(s instanceof ChangeDataSource)) {
     154           0 :       throw new QueryParseException("invalid query: " + s);
     155             :     }
     156         111 :     return s;
     157             :   }
     158             : 
     159             :   private Predicate<ChangeData> rewriteImpl(Predicate<ChangeData> in, QueryOptions opts)
     160             :       throws QueryParseException {
     161         111 :     ChangeIndex index = indexes.getSearchIndex();
     162             : 
     163         111 :     MutableInteger leafTerms = new MutableInteger();
     164         111 :     Predicate<ChangeData> out = rewriteImpl(in, index, opts, leafTerms);
     165         111 :     if (leafTerms.value > config.maxTerms()) {
     166           1 :       throw new TooManyTermsInQueryException(leafTerms.value, config.maxTerms());
     167             :     }
     168         111 :     if (isSameInstance(in, out) || out instanceof IndexPredicate) {
     169         111 :       return new IndexedChangeQuery(index, out, opts);
     170          10 :     } else if (out == null /* cannot rewrite */) {
     171           7 :       return in;
     172             :     } else {
     173          10 :       return out;
     174             :     }
     175             :   }
     176             : 
     177             :   /**
     178             :    * Rewrite a single predicate subtree.
     179             :    *
     180             :    * @param in predicate to rewrite.
     181             :    * @param index index whose schema determines which fields are indexed.
     182             :    * @param opts other query options.
     183             :    * @param leafTerms number of leaf index query terms encountered so far.
     184             :    * @return {@code null} if no part of this subtree can be queried in the index directly. {@code
     185             :    *     in} if this subtree and all its children can be queried directly in the index. Otherwise, a
     186             :    *     predicate that is semantically equivalent, with some of its subtrees wrapped to query the
     187             :    *     index directly.
     188             :    * @throws QueryParseException if the underlying index implementation does not support this
     189             :    *     predicate.
     190             :    */
     191             :   @Nullable
     192             :   private Predicate<ChangeData> rewriteImpl(
     193             :       Predicate<ChangeData> in, ChangeIndex index, QueryOptions opts, MutableInteger leafTerms)
     194             :       throws QueryParseException {
     195         111 :     in = IsSubmittablePredicate.rewrite(in);
     196         111 :     if (isIndexPredicate(in, index)) {
     197         111 :       ++leafTerms.value;
     198         111 :       return in;
     199         109 :     } else if (in instanceof LimitPredicate) {
     200             :       // Replace any limits with the limit provided by the caller. The caller
     201             :       // should have already searched the predicate tree for limit predicates
     202             :       // and included that in their limit computation.
     203           6 :       return new LimitPredicate<>(ChangeQueryBuilder.FIELD_LIMIT, opts.limit());
     204         109 :     } else if (!isRewritePossible(in)) {
     205           8 :       if (in instanceof IndexPredicate) {
     206           1 :         throw new QueryParseException("Unsupported index predicate: " + in.toString());
     207             :       }
     208           8 :       return null; // magic to indicate "in" cannot be rewritten
     209             :     }
     210             : 
     211         109 :     int n = in.getChildCount();
     212         109 :     BitSet isIndexed = new BitSet(n);
     213         109 :     BitSet notIndexed = new BitSet(n);
     214         109 :     BitSet rewritten = new BitSet(n);
     215         109 :     BitSet changeSource = new BitSet(n);
     216         109 :     List<Predicate<ChangeData>> newChildren = Lists.newArrayListWithCapacity(n);
     217         109 :     for (int i = 0; i < n; i++) {
     218         109 :       Predicate<ChangeData> c = in.getChild(i);
     219         109 :       Predicate<ChangeData> nc = rewriteImpl(c, index, opts, leafTerms);
     220         109 :       if (isSameInstance(nc, c)) {
     221         109 :         isIndexed.set(i);
     222         109 :         newChildren.add(c);
     223          10 :       } else if (nc == null /* cannot rewrite c */) {
     224           8 :         notIndexed.set(i);
     225           8 :         newChildren.add(c);
     226             :       } else {
     227           7 :         if (nc instanceof ChangeDataSource) {
     228           0 :           changeSource.set(i);
     229             :         }
     230           7 :         rewritten.set(i);
     231           7 :         newChildren.add(nc);
     232             :       }
     233             :     }
     234             : 
     235         109 :     if (isIndexed.cardinality() == n) {
     236         109 :       return in; // All children are indexed, leave as-is for parent.
     237          10 :     } else if (notIndexed.cardinality() == n) {
     238           1 :       return null; // Can't rewrite any children, so cannot rewrite in.
     239          10 :     } else if (rewritten.cardinality() == n) {
     240             :       // All children were rewritten.
     241           0 :       if (changeSource.cardinality() == n) {
     242           0 :         return copy(in, newChildren);
     243             :       }
     244           0 :       return in.copy(newChildren);
     245             :     }
     246          10 :     return partitionChildren(in, newChildren, isIndexed, index, opts);
     247             :   }
     248             : 
     249             :   private boolean isIndexPredicate(Predicate<ChangeData> in, ChangeIndex index) {
     250         111 :     if (!(in instanceof IndexPredicate)) {
     251         109 :       return false;
     252             :     }
     253         111 :     IndexPredicate<ChangeData> p = (IndexPredicate<ChangeData>) in;
     254             : 
     255         111 :     SchemaField<ChangeData, ?> field = p.getField();
     256         111 :     Schema<ChangeData> schema = index.getSchema();
     257         111 :     return schema.hasField(field);
     258             :   }
     259             : 
     260             :   private Predicate<ChangeData> partitionChildren(
     261             :       Predicate<ChangeData> in,
     262             :       List<Predicate<ChangeData>> newChildren,
     263             :       BitSet isIndexed,
     264             :       ChangeIndex index,
     265             :       QueryOptions opts)
     266             :       throws QueryParseException {
     267          10 :     if (isIndexed.cardinality() == 1) {
     268           9 :       int i = isIndexed.nextSetBit(0);
     269           9 :       Predicate<ChangeData> indexed = newChildren.remove(i);
     270           9 :       newChildren.add(0, new IndexedChangeQuery(index, copy(indexed, indexed.getChildren()), opts));
     271           9 :       return copy(in, newChildren);
     272             :     }
     273             : 
     274             :     // Group all indexed predicates into a wrapped subtree.
     275           6 :     List<Predicate<ChangeData>> indexed = Lists.newArrayListWithCapacity(isIndexed.cardinality());
     276             : 
     277           6 :     List<Predicate<ChangeData>> all =
     278           6 :         Lists.newArrayListWithCapacity(newChildren.size() - isIndexed.cardinality() + 1);
     279             : 
     280           6 :     for (int i = 0; i < newChildren.size(); i++) {
     281           6 :       Predicate<ChangeData> c = newChildren.get(i);
     282           6 :       if (isIndexed.get(i)) {
     283           6 :         indexed.add(c);
     284             :       } else {
     285           6 :         all.add(c);
     286             :       }
     287             :     }
     288           6 :     all.add(0, new IndexedChangeQuery(index, copy(in, indexed), opts));
     289           6 :     return copy(in, all);
     290             :   }
     291             : 
     292             :   private Predicate<ChangeData> copy(Predicate<ChangeData> in, List<Predicate<ChangeData>> all) {
     293          10 :     if (in instanceof AndPredicate) {
     294          10 :       Optional<Predicate<ChangeData>> atLeastOneChangeDataSource =
     295          10 :           all.stream().filter(p -> (p instanceof ChangeDataSource)).findAny();
     296          10 :       if (atLeastOneChangeDataSource.isPresent()) {
     297          10 :         return new AndChangeSource(all, config);
     298             :       }
     299           6 :       Optional<Predicate<ChangeData>> atLeastOneCardinalPredicate =
     300           6 :           all.stream().filter(p -> (p instanceof HasCardinality)).findAny();
     301           6 :       if (atLeastOneCardinalPredicate.isPresent()) {
     302           6 :         return new AndCardinalPredicate<>(all);
     303             :       }
     304           9 :     } else if (in instanceof OrPredicate) {
     305           7 :       Optional<Predicate<ChangeData>> nonChangeDataSource =
     306           7 :           all.stream().filter(p -> !(p instanceof ChangeDataSource)).findAny();
     307           7 :       if (!nonChangeDataSource.isPresent()) {
     308           0 :         return new OrSource(all);
     309             :       }
     310           7 :       Optional<Predicate<ChangeData>> nonHasCardinality =
     311           7 :           all.stream().filter(p -> !(p instanceof HasCardinality)).findAny();
     312           7 :       if (!nonHasCardinality.isPresent()) {
     313           7 :         return new OrCardinalPredicate<>(all);
     314             :       }
     315             :     }
     316           7 :     return in.copy(all);
     317             :   }
     318             : 
     319             :   private static boolean isRewritePossible(Predicate<ChangeData> p) {
     320         109 :     return p.getChildCount() > 0
     321             :         && (p instanceof AndPredicate || p instanceof OrPredicate || p instanceof NotPredicate);
     322             :   }
     323             : 
     324             :   @SuppressWarnings("ReferenceEquality")
     325             :   private static <T> boolean isSameInstance(T a, T b) {
     326         111 :     return a == b;
     327             :   }
     328             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750