blob: 8f5e36e8371580b90c8084a1891d14aaea60756f [file] [log] [blame]
// Copyright (C) 2013 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.index.change;
import static com.google.common.base.Preconditions.checkState;
import static com.google.gerrit.server.index.change.ChangeField.CHANGE_SPEC;
import static com.google.gerrit.server.index.change.ChangeField.NUMERIC_ID_STR_SPEC;
import static com.google.gerrit.server.index.change.ChangeField.PROJECT_SPEC;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.gerrit.entities.Change;
import com.google.gerrit.index.IndexConfig;
import com.google.gerrit.index.QueryOptions;
import com.google.gerrit.index.query.DataSource;
import com.google.gerrit.index.query.IndexPredicate;
import com.google.gerrit.index.query.IndexedQuery;
import com.google.gerrit.index.query.Matchable;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.index.query.ResultSet;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeDataSource;
import com.google.gerrit.server.query.change.ChangeIndexPostFilterPredicate;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
/**
* Wrapper combining an {@link IndexPredicate} together with a {@link ChangeDataSource} that returns
* matching results from the index.
*
* <p>Appropriate to return as the rootmost predicate that can be processed using the secondary
* index; such predicates must also implement {@link ChangeDataSource} to be chosen by the query
* processor.
*/
public class IndexedChangeQuery extends IndexedQuery<Change.Id, ChangeData>
implements ChangeDataSource, Matchable<ChangeData> {
public static QueryOptions oneResult() {
IndexConfig config = IndexConfig.createDefault();
return createOptions(config, 0, 1, config.pageSizeMultiplier(), 1, ImmutableSet.of());
}
public static QueryOptions createOptions(
IndexConfig config, int start, int limit, Set<String> fields) {
return createOptions(config, start, limit, config.pageSizeMultiplier(), limit, fields);
}
public static QueryOptions createOptions(
IndexConfig config,
int start,
int pageSize,
int pageSizeMultiplier,
int limit,
Set<String> fields) {
// Always include project and change id since both are needed to load the change from NoteDb.
if (!fields.contains(CHANGE_SPEC.getName())
&& !(fields.contains(PROJECT_SPEC.getName())
&& fields.contains(NUMERIC_ID_STR_SPEC.getName()))) {
fields = new HashSet<>(fields);
fields.add(PROJECT_SPEC.getName());
fields.add(NUMERIC_ID_STR_SPEC.getName());
}
return QueryOptions.create(config, start, pageSize, pageSizeMultiplier, limit, fields);
}
@VisibleForTesting
static QueryOptions convertOptions(QueryOptions opts) {
opts = opts.convertForBackend();
return IndexedChangeQuery.createOptions(
opts.config(),
opts.start(),
opts.pageSize(),
opts.pageSizeMultiplier(),
opts.limit(),
opts.fields());
}
private final Map<ChangeData, DataSource<ChangeData>> fromSource;
public IndexedChangeQuery(ChangeIndex index, Predicate<ChangeData> pred, QueryOptions opts)
throws QueryParseException {
super(index, pred, convertOptions(opts));
this.fromSource = new HashMap<>();
}
@Override
public ResultSet<ChangeData> read() {
final DataSource<ChangeData> currSource = source;
final ResultSet<ChangeData> rs = currSource.read();
return new ResultSet<>() {
@Override
public Iterator<ChangeData> iterator() {
return Iterables.transform(
rs,
cd -> {
fromSource.put(cd, currSource);
return cd;
})
.iterator();
}
@Override
public ImmutableList<ChangeData> toList() {
ImmutableList<ChangeData> r = rs.toList();
for (ChangeData cd : r) {
fromSource.put(cd, currSource);
}
return r;
}
@Override
public void close() {
rs.close();
}
@Override
public Object searchAfter() {
return rs.searchAfter();
}
};
}
public boolean postIndexMatch(Predicate<ChangeData> pred, ChangeData cd) {
if (pred instanceof ChangeIndexPostFilterPredicate) {
checkState(
pred.isMatchable(),
"match invoked, but child predicate %s doesn't implement %s",
pred,
Matchable.class.getName());
return pred.asMatchable().match(cd);
}
for (int i = 0; i < pred.getChildCount(); i++) {
if (!postIndexMatch(pred.getChild(i), cd)) {
return false;
}
}
return true;
}
@Override
public boolean match(ChangeData cd) {
Predicate<ChangeData> pred = getChild(0);
if (source != null && fromSource.get(cd) == source && postIndexMatch(pred, cd)) {
return true;
}
checkState(
pred.isMatchable(),
"match invoked, but child predicate %s doesn't implement %s",
pred,
Matchable.class.getName());
return pred.asMatchable().match(cd);
}
@Override
public int getCost() {
// Index queries are assumed to be cheaper than any other type of query, so
// so try to make sure they get picked. Note that pred's cost may be higher
// because it doesn't know whether it's being used in an index query or not.
return 1;
}
@Override
public boolean hasChange() {
return index.getSchema().hasField(ChangeField.CHANGE_SPEC);
}
}