Merge changes from topics 'query-refactor', 'kill-sortkey'

* changes:
  QueryProcessor: Don't double-add visibleto predicate
  Add query tests for visibleto predicate
  Terminate sortkey with prejudice
  Use secondary index for advertising extra haves during push
  OutputStreamQuery: Optimize formatter allocation
  Move stream-based QueryProcessor output to its own class
  Add QueryProcessor methods to search by Predicate
  Fix limit handling in QueryProcessor
  QueryChanges: Remove unused reverse field
This commit is contained in:
Shawn Pearce
2014-12-24 17:19:57 +00:00
committed by Gerrit Code Review
39 changed files with 826 additions and 639 deletions

View File

@@ -116,7 +116,7 @@ Find the 2 most recent open changes in the tools/gerrit project:
====
$ ssh -p 29418 review.example.com gerrit query --format=JSON status:open project:tools/gerrit limit:2
{"project":"tools/gerrit", ...}
{"project":"tools/gerrit", ..., sortKey:"000e6aee00003e26", ...}
{"project":"tools/gerrit", ...}
{"type":"stats","rowCount":2,"runningTimeMilliseconds:15}
====

View File

@@ -35,8 +35,6 @@ was created.
lastUpdated:: Time in seconds since the UNIX epoch when this change
was last updated.
sortKey:: Internal key used to sort changes, based on lastUpdated.
open:: Boolean indicating if the change is still open for review.
status:: Current state of this change.

View File

@@ -1123,7 +1123,6 @@ link:rest-api-changes.html#change-info[ChangeInfo] entities.
"created": "2013-02-01 09:59:32.126000000",
"updated": "2013-02-21 11:16:36.775000000",
"mergeable": true,
"_sortkey": "0023412400000f7d",
"_number": 3965,
"owner": {
"name": "John Doe"

View File

@@ -55,7 +55,6 @@ the resulting change.
"mergeable": true,
"insertions": 0,
"deletions": 0,
"_sortkey": "002cbc25000004e5",
"_number": 4711,
"owner": {
"name": "John Doe"
@@ -105,7 +104,6 @@ Query for open changes of watched projects:
"mergeable": true,
"insertions": 26,
"deletions": 10,
"_sortkey": "001e7057000006dc",
"_number": 1756,
"owner": {
"name": "John Doe"
@@ -123,7 +121,6 @@ Query for open changes of watched projects:
"mergeable": true,
"insertions": 12,
"deletions": 18,
"_sortkey": "001e7056000006dd",
"_number": 1757,
"owner": {
"name": "John Doe"
@@ -177,7 +174,6 @@ Query that retrieves changes for a user's dashboard:
"mergeable": true,
"insertions": 4,
"deletions": 7,
"_sortkey": "001e7057000006dc",
"_number": 1756,
"owner": {
"name": "John Doe"
@@ -330,7 +326,6 @@ default. Optional fields are:
"mergeable": true,
"insertions": 16,
"deletions": 7,
"_sortkey": "001c9bf400000061",
"_number": 97,
"owner": {
"name": "Shawn Pearce"
@@ -467,7 +462,6 @@ describes the change.
"mergeable": true,
"insertions": 34,
"deletions": 101,
"_sortkey": "0023412400000f7d",
"_number": 3965,
"owner": {
"name": "John Doe"
@@ -520,7 +514,6 @@ REJECTED > APPROVED > DISLIKED > RECOMMENDED.
"mergeable": true,
"insertions": 126,
"deletions": 11,
"_sortkey": "0023412400000f7d",
"_number": 3965,
"owner": {
"_account_id": 1000096,
@@ -764,7 +757,6 @@ describes the abandoned change.
"mergeable": true,
"insertions": 3,
"deletions": 310,
"_sortkey": "0023412400000f7d",
"_number": 3965,
"owner": {
"name": "John Doe"
@@ -823,7 +815,6 @@ describes the restored change.
"mergeable": true,
"insertions": 2,
"deletions": 13,
"_sortkey": "0023412400000f7d",
"_number": 3965,
"owner": {
"name": "John Doe"
@@ -880,7 +871,6 @@ is included.
"mergeable": false,
"insertions": 33,
"deletions": 9,
"_sortkey": "0024cf9a000012bf",
"_number": 4799,
"owner": {
"name": "John Doe"
@@ -973,7 +963,6 @@ describes the reverting change.
"mergeable": true,
"insertions": 6,
"deletions": 4,
"_sortkey": "0023412400000f7d",
"_number": 3965,
"owner": {
"name": "John Doe"
@@ -1035,7 +1024,6 @@ describes the submitted/merged change.
"status": "MERGED",
"created": "2013-02-01 09:59:32.126000000",
"updated": "2013-02-21 11:16:36.775000000",
"_sortkey": "0023412400000f7d",
"_number": 3965,
"owner": {
"name": "John Doe"
@@ -1179,7 +1167,6 @@ missing from the result. At least `id`, `project`, `branch`, and
"mergeable": true,
"insertions": 34,
"deletions": 101,
"_sortkey": "0023412400000f7d",
"_number": 3965,
"owner": {
"name": "John Doe"
@@ -1228,7 +1215,6 @@ Only the change owner, a project owner, or an administrator may fix changes.
"mergeable": true,
"insertions": 34,
"deletions": 101,
"_sortkey": "0023412400000f7d",
"_number": 3965,
"owner": {
"name": "John Doe"
@@ -1863,7 +1849,6 @@ for the current patch set.
"mergeable": true,
"insertions": 34,
"deletions": 45,
"_sortkey": "0023412400000f7d",
"_number": 3965,
"owner": {
"_account_id": 1000096,
@@ -2128,7 +2113,6 @@ is included.
"mergeable": false,
"insertions": 21,
"deletions": 21,
"_sortkey": "0024cf9a000012bf",
"_number": 4799,
"owner": {
"name": "John Doe"
@@ -3096,7 +3080,6 @@ describes the resulting cherry picked change.
"mergeable": true,
"insertions": 12,
"deletions": 11,
"_sortkey": "0023412400000f7d",
"_number": 3965,
"owner": {
"name": "John Doe"
@@ -3150,7 +3133,6 @@ describes the change.
"mergeable": true,
"insertions": 261,
"deletions": 101,
"_sortkey": "0023412400000f7d",
"_number": 3965,
"owner": {
"name": "John Doe"
@@ -3335,7 +3317,6 @@ Not set for merged changes, or if the change has not yet been tested.
Number of inserted lines.
|`deletions` ||
Number of deleted lines.
|`_sortkey` ||The sortkey of the change.
|`_number` ||The legacy numeric ID of the change.
|`owner` ||
The owner of the change as an link:rest-api-accounts.html#account-info[
@@ -3373,7 +3354,7 @@ Only set if link:#current-revision[the current revision] is requested
if link:#all-revisions[all revisions] are requested.
|`_more_changes` |optional, not set if `false`|
Whether the query would deliver more results if not limited. +
Only set on either the last or the first change that is returned.
Only set on the last change that is returned.
|`problems` |optional|
A list of link:#problem-info[ProblemInfo] entities describing potential
problems with this change. Only set if link:#check[CHECK] is set.

View File

@@ -31,7 +31,6 @@ public class ChangeInfo {
protected String topic;
protected boolean starred;
protected Timestamp lastUpdatedOn;
protected String sortKey;
protected PatchSet.Id patchSetId;
protected boolean latest;
@@ -52,7 +51,6 @@ public class ChangeInfo {
branch = c.getDest().getShortName();
topic = c.getTopic();
lastUpdatedOn = c.getLastUpdatedOn();
sortKey = c.getSortKey();
patchSetId = patchId;
latest = patchSetId == null || patchSetId.equals(c.currentPatchSetId());
}
@@ -112,8 +110,4 @@ public class ChangeInfo {
public java.sql.Timestamp getLastUpdatedOn() {
return lastUpdatedOn;
}
public String getSortKey() {
return sortKey;
}
}

View File

@@ -36,7 +36,6 @@ public class ChangeInfo {
public Integer insertions;
public Integer deletions;
public String _sortkey;
public String baseChange;
public int _number;

View File

@@ -96,7 +96,6 @@ public class ChangeInfo extends JavaScriptObject {
private final native String updatedRaw() /*-{ return this.updated; }-*/;
public final native boolean starred() /*-{ return this.starred ? true : false; }-*/;
public final native boolean reviewed() /*-{ return this.reviewed ? true : false; }-*/;
public final native String _sortkey() /*-{ return this._sortkey; }-*/;
public final native NativeMap<LabelInfo> all_labels() /*-{ return this.labels; }-*/;
public final native LabelInfo label(String n) /*-{ return this.labels[n]; }-*/;
public final native String current_revision() /*-{ return this.current_revision; }-*/;

View File

@@ -14,8 +14,8 @@
package com.google.gerrit.httpd.rpc.change;
import com.google.gerrit.server.query.change.QueryProcessor;
import com.google.gerrit.server.query.change.QueryProcessor.OutputFormat;
import com.google.gerrit.server.query.change.OutputStreamQuery;
import com.google.gerrit.server.query.change.OutputStreamQuery.OutputFormat;
import com.google.gson.Gson;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -31,11 +31,11 @@ import javax.servlet.http.HttpServletResponse;
@Singleton
public class DeprecatedChangeQueryServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private final Provider<QueryProcessor> processor;
private final Provider<OutputStreamQuery> queryProvider;
@Inject
DeprecatedChangeQueryServlet(Provider<QueryProcessor> processor) {
this.processor = processor;
DeprecatedChangeQueryServlet(Provider<OutputStreamQuery> queryProvider) {
this.queryProvider = queryProvider;
}
@Override
@@ -44,7 +44,7 @@ public class DeprecatedChangeQueryServlet extends HttpServlet {
rsp.setContentType("text/json");
rsp.setCharacterEncoding("UTF-8");
QueryProcessor p = processor.get();
OutputStreamQuery p = queryProvider.get();
OutputFormat format = OutputFormat.JSON;
try {
format = OutputFormat.valueOf(get(req, "format", format.toString()));

View File

@@ -433,9 +433,7 @@ public final class Change {
@Column(id = 5)
protected Timestamp lastUpdatedOn;
/** A {@link #lastUpdatedOn} ASC,{@link #changeId} ASC for sorting. */
@Column(id = 6, length = 16)
protected String sortKey;
// DELETED: id = 6 (sortkey)
@Column(id = 7, name = "owner_account_id")
protected Account.Id owner;
@@ -489,7 +487,6 @@ public final class Change {
rowVersion = other.rowVersion;
createdOn = other.createdOn;
lastUpdatedOn = other.lastUpdatedOn;
sortKey = other.sortKey;
owner = other.owner;
dest = other.dest;
open = other.open;
@@ -534,14 +531,6 @@ public final class Change {
return rowVersion;
}
public String getSortKey() {
return sortKey;
}
public void setSortKey(final String newSortKey) {
sortKey = newSortKey;
}
public Account.Id getOwner() {
return owner;
}

View File

@@ -55,11 +55,6 @@ public interface ChangeAccess extends Access<Change, Change.Id> {
@Query("WHERE open = true AND dest = ?")
ResultSet<Change> byBranchOpenAll(Branch.NameKey p) throws OrmException;
@Query("WHERE open = true AND dest.projectName = ? AND sortKey < ?"
+ " ORDER BY sortKey DESC LIMIT ?")
ResultSet<Change> byProjectOpenNext(Project.NameKey p, String sortKey,
int limit) throws OrmException;
@Query
ResultSet<Change> all() throws OrmException;
}

View File

@@ -75,7 +75,7 @@ ON changes (status, dest_project_name, dest_branch_name, last_updated_on);
-- covers: byProjectOpenAll
CREATE INDEX changes_byProjectOpen
ON changes (open, dest_project_name, sort_key);
ON changes (open, dest_project_name, last_updated_on);
-- covers: byProject
CREATE INDEX changes_byProject

View File

@@ -83,7 +83,7 @@ ON changes (status, dest_project_name, dest_branch_name, last_updated_on)
-- covers: byProjectOpenPrev, byProjectOpenNext
CREATE INDEX changes_byProjectOpen
ON changes (open, dest_project_name, sort_key)
ON changes (open, dest_project_name, last_updated_on);
#
-- covers: byProject

View File

@@ -124,7 +124,7 @@ WHERE status = 's';
-- covers: byProjectOpenAll
CREATE INDEX changes_byProjectOpen
ON changes (dest_project_name, sort_key)
ON changes (dest_project_name, last_updated_on)
WHERE open = 'Y';
-- covers: byProject

View File

@@ -15,15 +15,10 @@
package com.google.gerrit.server;
import static com.google.gerrit.server.change.PatchSetInserter.ValidatePolicy.RECEIVE_COMMITS;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.primitives.Ints;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.reviewdb.client.Change;
@@ -85,15 +80,6 @@ import java.util.Map;
@Singleton
public class ChangeUtil {
/**
* Epoch for sort key calculations, Tue Sep 30 2008 17:00:00.
* <p>
* We overrun approximately 4,083 years later, so ~6092.
*/
@VisibleForTesting
private static final long SORT_KEY_EPOCH_MINS =
MINUTES.convert(1222819200L, SECONDS);
private static final Object uuidLock = new Object();
private static final int SEED = 0x2418e6f9;
private static int uuidPrefix;
@@ -150,7 +136,6 @@ public class ChangeUtil {
public static void updated(Change c) {
c.setLastUpdatedOn(TimeUtil.nowTs());
computeSortKey(c);
}
public static void insertAncestors(ReviewDb db, PatchSet.Id id, RevCommit src)
@@ -166,29 +151,6 @@ public class ChangeUtil {
db.patchSetAncestors().insert(toInsert);
}
public static String sortKey(long lastUpdatedMs, int id) {
long lastUpdatedMins = MINUTES.convert(lastUpdatedMs, MILLISECONDS);
long minsSinceEpoch = lastUpdatedMins - SORT_KEY_EPOCH_MINS;
StringBuilder r = new StringBuilder(16);
r.setLength(16);
formatHexInt(r, 0, Ints.checkedCast(minsSinceEpoch));
formatHexInt(r, 8, id);
return r.toString();
}
public static long parseSortKey(String sortKey) {
if ("z".equals(sortKey)) {
return Long.MAX_VALUE;
}
return Long.parseLong(sortKey, 16);
}
public static void computeSortKey(Change c) {
long lastUpdatedMs = c.getLastUpdatedOn().getTime();
int id = c.getId().get();
c.setSortKey(sortKey(lastUpdatedMs, id));
}
public static PatchSet.Id nextPatchSetId(Map<String, Ref> allRefs,
PatchSet.Id id) {
PatchSet.Id next = nextPatchSetId(id);
@@ -585,19 +547,4 @@ public class ChangeUtil {
public static PatchSet.Id nextPatchSetId(PatchSet.Id id) {
return new PatchSet.Id(id.getParentKey(), id.get() + 1);
}
private static final char[] hexchar =
{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', //
'a', 'b', 'c', 'd', 'e', 'f'};
private static void formatHexInt(final StringBuilder dst, final int p, int w) {
int o = p + 7;
while (o >= p && w != 0) {
dst.setCharAt(o--, hexchar[w & 0xf]);
w >>>= 4;
}
while (o >= p) {
dst.setCharAt(o--, '0');
}
}
}

View File

@@ -136,7 +136,6 @@ public class ChangeInserter {
patchSet.setRevision(new RevId(commit.name()));
patchSetInfo = patchSetInfoFactory.get(commit, patchSet.getId());
change.setCurrentPatchSet(patchSetInfo);
ChangeUtil.computeSortKey(change);
}
public Change getChange() {

View File

@@ -104,6 +104,7 @@ import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.SubmitRuleEvaluator;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeData.ChangedLines;
import com.google.gerrit.server.query.change.QueryResult;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -252,10 +253,16 @@ public class ChangeJson {
return format(cd, Optional.of(rsrc.getPatchSet().getId()));
}
public List<List<ChangeInfo>> formatList2(List<List<ChangeData>> in)
public List<List<ChangeInfo>> formatQueryResults(List<QueryResult> in)
throws OrmException {
accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
Iterable<ChangeData> all = Iterables.concat(in);
Iterable<ChangeData> all = FluentIterable.from(in)
.transformAndConcat(new Function<QueryResult, List<ChangeData>>() {
@Override
public List<ChangeData> apply(QueryResult in) {
return in.changes();
}
});
ChangeData.ensureChangeLoaded(all);
if (has(ALL_REVISIONS)) {
ChangeData.ensureAllPatchSetsLoaded(all);
@@ -270,8 +277,12 @@ public class ChangeJson {
List<List<ChangeInfo>> res = Lists.newArrayListWithCapacity(in.size());
Map<Change.Id, ChangeInfo> out = Maps.newHashMap();
for (List<ChangeData> changes : in) {
res.add(toChangeInfo(out, changes, reviewed));
for (QueryResult r : in) {
List<ChangeInfo> infos = toChangeInfo(out, r.changes(), reviewed);
if (r.moreChanges()) {
infos.get(infos.size() - 1)._moreChanges = true;
}
res.add(infos);
}
accountLoader.fill();
return res;
@@ -368,7 +379,6 @@ public class ChangeJson {
out.created = in.getCreatedOn();
out.updated = in.getLastUpdatedOn();
out._number = in.getId().get();
out._sortkey = in.getSortKey();
out.starred = userProvider.get().getStarredChanges().contains(in.getId())
? true
: null;

View File

@@ -47,7 +47,6 @@ import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.ProjectUtil;
@@ -271,7 +270,6 @@ public class Submit implements RestModifyView<RevisionResource, SubmitInput>,
if (change.getStatus().isOpen()) {
change.setStatus(Change.Status.SUBMITTED);
change.setLastUpdatedOn(timestamp);
ChangeUtil.computeSortKey(change);
return change;
}
return null;

View File

@@ -31,7 +31,6 @@ public class ChangeAttribute {
public Long createdOn;
public Long lastUpdated;
public String sortKey;
public Boolean open;
public Change.Status status;
public List<MessageAttribute> comments;

View File

@@ -18,5 +18,5 @@ public class QueryStatsAttribute {
public final String type = "stats";
public int rowCount;
public long runTimeMilliseconds;
public String resumeSortKey;
public boolean moreChanges;
}

View File

@@ -164,7 +164,6 @@ public class EventFactory {
public void extend(ChangeAttribute a, Change change) {
a.createdOn = change.getCreatedOn().getTime() / 1000L;
a.lastUpdated = change.getLastUpdatedOn().getTime() / 1000L;
a.sortKey = change.getSortKey();
a.open = change.getStatus().isOpen();
}

View File

@@ -111,6 +111,7 @@ import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.project.RefControl;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.QueryProcessor;
import com.google.gerrit.server.ssh.SshInfo;
import com.google.gerrit.server.util.LabelVote;
import com.google.gerrit.server.util.MagicBranch;
@@ -336,6 +337,7 @@ public class ReceiveCommits {
@Inject
ReceiveCommits(final ReviewDb db,
final Provider<QueryProcessor> queryProcessor,
final SchemaFactory<ReviewDb> schemaFactory,
final ChangeData.Factory changeDataFactory,
final ChangeUpdate.Factory updateFactory,
@@ -472,7 +474,7 @@ public class ReceiveCommits {
});
advHooks.add(rp.getAdvertiseRefsHook());
advHooks.add(new ReceiveCommitsAdvertiseRefsHook(
db, projectControl.getProject().getNameKey()));
db, queryProcessor, projectControl.getProject().getNameKey()));
rp.setAdvertiseRefsHook(AdvertiseRefsHookChain.newChain(advHooks));
}

View File

@@ -18,13 +18,18 @@ import static org.eclipse.jgit.lib.RefDatabase.ALL;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.query.Predicate;
import com.google.gerrit.server.query.QueryParseException;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import com.google.gerrit.server.query.change.QueryProcessor;
import com.google.gerrit.server.util.MagicBranch;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Provider;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
@@ -39,6 +44,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -48,11 +54,14 @@ public class ReceiveCommitsAdvertiseRefsHook implements AdvertiseRefsHook {
.getLogger(ReceiveCommitsAdvertiseRefsHook.class);
private final ReviewDb db;
private final Provider<QueryProcessor> queryProcessor;
private final Project.NameKey projectName;
public ReceiveCommitsAdvertiseRefsHook(ReviewDb db,
Provider<QueryProcessor> queryProcessor,
Project.NameKey projectName) {
this.db = db;
this.queryProcessor = queryProcessor;
this.projectName = projectName;
}
@@ -93,10 +102,11 @@ public class ReceiveCommitsAdvertiseRefsHook implements AdvertiseRefsHook {
Set<ObjectId> toInclude = Sets.newHashSet();
// Advertise some recent open changes, in case a commit is based one.
final int limit = 32;
try {
Set<PatchSet.Id> toGet = Sets.newHashSetWithExpectedSize(32);
for (Change c : db.changes().byProjectOpenNext(projectName, "z", 32)) {
PatchSet.Id id = c.currentPatchSetId();
Set<PatchSet.Id> toGet = Sets.newHashSetWithExpectedSize(limit);
for (ChangeData cd : queryRecentChanges(limit)) {
PatchSet.Id id = cd.change().currentPatchSetId();
if (id != null) {
toGet.add(id);
}
@@ -164,6 +174,20 @@ public class ReceiveCommitsAdvertiseRefsHook implements AdvertiseRefsHook {
return toInclude;
}
private List<ChangeData> queryRecentChanges(int limit)
throws OrmException {
QueryProcessor qp = queryProcessor.get();
qp.setLimit(limit);
ChangeQueryBuilder qb = qp.getQueryBuilder();
Predicate<ChangeData> p =
Predicate.and(qb.project(projectName.get()), qb.status_open());
try {
return qp.queryChanges(p).changes();
} catch (QueryParseException e) {
throw new OrmException(e);
}
}
private static boolean skip(String name) {
return name.startsWith(RefNames.REFS_CHANGES)
|| name.startsWith(RefNames.REFS_CACHE_AUTOMERGE)

View File

@@ -14,9 +14,8 @@
package com.google.gerrit.server.index;
import static com.google.gerrit.common.data.GlobalCapability.DEFAULT_MAX_QUERY_LIMIT;
import static com.google.common.base.Preconditions.checkArgument;
import com.google.common.base.MoreObjects;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.gerrit.reviewdb.client.Change;
@@ -29,9 +28,9 @@ import com.google.gerrit.server.query.QueryParseException;
import com.google.gerrit.server.query.change.AndSource;
import com.google.gerrit.server.query.change.BasicChangeRewrites;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import com.google.gerrit.server.query.change.ChangeQueryRewriter;
import com.google.gerrit.server.query.change.ChangeStatusPredicate;
import com.google.gerrit.server.query.change.LimitPredicate;
import com.google.gerrit.server.query.change.OrSource;
import com.google.inject.Inject;
@@ -129,16 +128,14 @@ public class IndexRewriteImpl implements ChangeQueryRewriter {
}
@Override
public Predicate<ChangeData> rewrite(Predicate<ChangeData> in, int start)
throws QueryParseException {
public Predicate<ChangeData> rewrite(Predicate<ChangeData> in, int start,
int limit) throws QueryParseException {
checkArgument(limit > 0, "limit must be positive: %s", limit);
ChangeIndex index = indexes.getSearchIndex();
in = basicRewrites.rewrite(in);
int limit = MoreObjects.firstNonNull(
ChangeQueryBuilder.getLimit(in), DEFAULT_MAX_QUERY_LIMIT);
// Increase the limit rather than skipping, since we don't know how many
// skipped results would have been filtered out by the enclosing AndSource.
limit += start;
limit = Math.max(limit, 1);
Predicate<ChangeData> out = rewriteImpl(in, index, limit);
if (in == out || out instanceof IndexPredicate) {
@@ -168,6 +165,9 @@ public class IndexRewriteImpl implements ChangeQueryRewriter {
ChangeIndex index, int limit) throws QueryParseException {
if (isIndexPredicate(in, index)) {
return in;
} else if (in instanceof LimitPredicate) {
// Replace any limits with the limit provided by the caller.
return new LimitPredicate(limit);
} else if (!isRewritePossible(in)) {
return null; // magic to indicate "in" cannot be rewritten
}

View File

@@ -138,7 +138,8 @@ public abstract class QueryBuilder<T> {
* @param p the predicate to find.
* @param clazz type of the predicate instance.
* @param name name of the operator.
* @return the predicate, null if not found.
* @return the first instance of a predicate having the given type, as found
* by a depth-first search.
*/
@SuppressWarnings("unchecked")
public static <T, P extends OperatorPredicate<T>> P find(Predicate<T> p,

View File

@@ -16,13 +16,11 @@ package com.google.gerrit.server.query.change;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.query.IntPredicate;
import com.google.gerrit.server.query.Predicate;
import com.google.gerrit.server.query.QueryRewriter;
import com.google.inject.Inject;
import com.google.inject.OutOfScopeException;
import com.google.inject.Provider;
import com.google.inject.name.Named;
public class BasicChangeRewrites extends QueryRewriter<ChangeData> {
private static final ChangeQueryBuilder BUILDER = new ChangeQueryBuilder(
@@ -69,14 +67,6 @@ public class BasicChangeRewrites extends QueryRewriter<ChangeData> {
ChangeStatusPredicate.forStatus(Change.Status.MERGED));
}
@NoCostComputation
@Rewrite("A=(limit:*) B=(limit:*)")
public Predicate<ChangeData> r00_smallestLimit(
@Named("A") IntPredicate<ChangeData> a,
@Named("B") IntPredicate<ChangeData> b) {
return a.intValue() <= b.intValue() ? a : b;
}
private static final class InvalidProvider<T> implements Provider<T> {
@Override
public T get() {

View File

@@ -45,7 +45,6 @@ import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.ListChildProjects;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.query.IntPredicate;
import com.google.gerrit.server.query.Predicate;
import com.google.gerrit.server.query.QueryBuilder;
import com.google.gerrit.server.query.QueryParseException;
@@ -121,12 +120,6 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
private static final QueryBuilder.Definition<ChangeData, ChangeQueryBuilder> mydef =
new QueryBuilder.Definition<>(ChangeQueryBuilder.class);
@SuppressWarnings("unchecked")
public static Integer getLimit(Predicate<ChangeData> p) {
IntPredicate<?> ip = find(p, IntPredicate.class, FIELD_LIMIT);
return ip != null ? ip.intValue() : null;
}
@VisibleForTesting
public static class Arguments {
final Provider<ReviewDb> db;
@@ -633,28 +626,8 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
}
@Operator
public Predicate<ChangeData> limit(String limit) {
return limit(Integer.parseInt(limit));
}
static class LimitPredicate extends IntPredicate<ChangeData> {
LimitPredicate(int limit) {
super(FIELD_LIMIT, limit);
}
@Override
public boolean match(ChangeData object) {
return true;
}
@Override
public int getCost() {
return 0;
}
}
public Predicate<ChangeData> limit(int limit) {
return new LimitPredicate(limit);
public Predicate<ChangeData> limit(String limit) throws QueryParseException {
return new LimitPredicate(Integer.parseInt(limit));
}
@Operator

View File

@@ -18,6 +18,6 @@ import com.google.gerrit.server.query.Predicate;
import com.google.gerrit.server.query.QueryParseException;
public interface ChangeQueryRewriter {
Predicate<ChangeData> rewrite(Predicate<ChangeData> in, int start)
Predicate<ChangeData> rewrite(Predicate<ChangeData> in, int start, int limit)
throws QueryParseException;
}

View File

@@ -0,0 +1,47 @@
// Copyright (C) 2014 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.query.change.ChangeQueryBuilder.FIELD_LIMIT;
import com.google.gerrit.server.query.IntPredicate;
import com.google.gerrit.server.query.Predicate;
import com.google.gerrit.server.query.QueryBuilder;
import com.google.gerrit.server.query.QueryParseException;
public class LimitPredicate extends IntPredicate<ChangeData> {
@SuppressWarnings("unchecked")
public static Integer getLimit(Predicate<ChangeData> p) {
IntPredicate<?> ip = QueryBuilder.find(p, IntPredicate.class, FIELD_LIMIT);
return ip != null ? ip.intValue() : null;
}
public LimitPredicate(int limit) throws QueryParseException {
super(ChangeQueryBuilder.FIELD_LIMIT, limit);
if (limit <= 0) {
throw new QueryParseException("limit must be positive: " + limit);
}
}
@Override
public boolean match(ChangeData object) {
return true;
}
@Override
public int getCost() {
return 0;
}
}

View File

@@ -0,0 +1,405 @@
// Copyright (C) 2014 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.common.TimeUtil;
import com.google.gerrit.common.data.LabelTypes;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.config.TrackingFooters;
import com.google.gerrit.server.data.ChangeAttribute;
import com.google.gerrit.server.data.PatchSetAttribute;
import com.google.gerrit.server.data.QueryStatsAttribute;
import com.google.gerrit.server.events.EventFactory;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.project.SubmitRuleEvaluator;
import com.google.gerrit.server.query.QueryParseException;
import com.google.gson.Gson;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import org.eclipse.jgit.util.io.DisabledOutputStream;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
/**
* Change query implementation that outputs to a stream in the style of an SSH
* command.
*/
public class OutputStreamQuery {
private static final Logger log =
LoggerFactory.getLogger(OutputStreamQuery.class);
private static final DateTimeFormatter dtf =
DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss zzz");
public static enum OutputFormat {
TEXT, JSON
}
private final QueryProcessor queryProcessor;
private final EventFactory eventFactory;
private final TrackingFooters trackingFooters;
private final CurrentUser user;
private OutputFormat outputFormat = OutputFormat.TEXT;
private boolean includePatchSets;
private boolean includeCurrentPatchSet;
private boolean includeApprovals;
private boolean includeComments;
private boolean includeFiles;
private boolean includeCommitMessage;
private boolean includeDependencies;
private boolean includeSubmitRecords;
private boolean includeAllReviewers;
private OutputStream outputStream = DisabledOutputStream.INSTANCE;
private PrintWriter out;
@Inject
OutputStreamQuery(QueryProcessor queryProcessor,
EventFactory eventFactory,
TrackingFooters trackingFooters,
CurrentUser user) {
this.queryProcessor = queryProcessor;
this.eventFactory = eventFactory;
this.trackingFooters = trackingFooters;
this.user = user;
}
void setLimit(int n) {
queryProcessor.setLimit(n);
}
public void setStart(int n) {
queryProcessor.setStart(n);
}
public void setIncludePatchSets(boolean on) {
includePatchSets = on;
}
public boolean getIncludePatchSets() {
return includePatchSets;
}
public void setIncludeCurrentPatchSet(boolean on) {
includeCurrentPatchSet = on;
}
public boolean getIncludeCurrentPatchSet() {
return includeCurrentPatchSet;
}
public void setIncludeApprovals(boolean on) {
includeApprovals = on;
}
public void setIncludeComments(boolean on) {
includeComments = on;
}
public void setIncludeFiles(boolean on) {
includeFiles = on;
}
public boolean getIncludeFiles() {
return includeFiles;
}
public void setIncludeDependencies(boolean on) {
includeDependencies = on;
}
public boolean getIncludeDependencies() {
return includeDependencies;
}
public void setIncludeCommitMessage(boolean on) {
includeCommitMessage = on;
}
public void setIncludeSubmitRecords(boolean on) {
includeSubmitRecords = on;
}
public void setIncludeAllReviewers(boolean on) {
includeAllReviewers = on;
}
public void setOutput(OutputStream out, OutputFormat fmt) {
this.outputStream = out;
this.outputFormat = fmt;
}
public void query(String queryString) throws IOException {
out = new PrintWriter( //
new BufferedWriter( //
new OutputStreamWriter(outputStream, "UTF-8")));
try {
if (queryProcessor.isDisabled()) {
ErrorMessage m = new ErrorMessage();
m.message = "query disabled";
show(m);
return;
}
try {
final QueryStatsAttribute stats = new QueryStatsAttribute();
stats.runTimeMilliseconds = TimeUtil.nowMs();
QueryResult results = queryProcessor.queryByString(queryString);
ChangeAttribute c = null;
for (ChangeData d : results.changes()) {
ChangeControl cc = d.changeControl().forUser(user);
LabelTypes labelTypes = cc.getLabelTypes();
c = eventFactory.asChangeAttribute(d.change());
eventFactory.extend(c, d.change());
if (!trackingFooters.isEmpty()) {
eventFactory.addTrackingIds(c,
trackingFooters.extract(d.commitFooters()));
}
if (includeAllReviewers) {
eventFactory.addAllReviewers(c, d.notes());
}
if (includeSubmitRecords) {
eventFactory.addSubmitRecords(c, new SubmitRuleEvaluator(d)
.setAllowClosed(true)
.setAllowDraft(true)
.canSubmit());
}
if (includeCommitMessage) {
eventFactory.addCommitMessage(c, d.commitMessage());
}
if (includePatchSets) {
if (includeFiles) {
eventFactory.addPatchSets(c, d.patches(),
includeApprovals ? d.approvals().asMap() : null,
includeFiles, d.change(), labelTypes);
} else {
eventFactory.addPatchSets(c, d.patches(),
includeApprovals ? d.approvals().asMap() : null,
labelTypes);
}
}
if (includeCurrentPatchSet) {
PatchSet current = d.currentPatchSet();
if (current != null) {
c.currentPatchSet = eventFactory.asPatchSetAttribute(current);
eventFactory.addApprovals(c.currentPatchSet,
d.currentApprovals(), labelTypes);
if (includeFiles) {
eventFactory.addPatchSetFileNames(c.currentPatchSet,
d.change(), d.currentPatchSet());
}
}
}
if (includeComments) {
eventFactory.addComments(c, d.messages());
if (includePatchSets) {
for (PatchSetAttribute attribute : c.patchSets) {
eventFactory.addPatchSetComments(attribute, d.publishedComments());
}
}
}
if (includeDependencies) {
eventFactory.addDependencies(c, d.change());
}
show(c);
}
stats.rowCount = results.changes().size();
stats.moreChanges = results.moreChanges();
stats.runTimeMilliseconds =
TimeUtil.nowMs() - stats.runTimeMilliseconds;
show(stats);
} catch (OrmException err) {
log.error("Cannot execute query: " + queryString, err);
ErrorMessage m = new ErrorMessage();
m.message = "cannot query database";
show(m);
} catch (QueryParseException e) {
ErrorMessage m = new ErrorMessage();
m.message = e.getMessage();
show(m);
} catch (NoSuchChangeException e) {
log.error("Missing change: " + e.getMessage(), e);
ErrorMessage m = new ErrorMessage();
m.message = "missing change " + e.getMessage();
show(m);
}
} finally {
try {
out.flush();
} finally {
out = null;
}
}
}
private void show(Object data) {
switch (outputFormat) {
default:
case TEXT:
if (data instanceof ChangeAttribute) {
out.print("change ");
out.print(((ChangeAttribute) data).id);
out.print("\n");
showText(data, 1);
} else {
showText(data, 0);
}
out.print('\n');
break;
case JSON:
out.print(new Gson().toJson(data));
out.print('\n');
break;
}
}
private void showText(Object data, int depth) {
for (Field f : fieldsOf(data.getClass())) {
Object val;
try {
val = f.get(data);
} catch (IllegalArgumentException err) {
continue;
} catch (IllegalAccessException err) {
continue;
}
if (val == null) {
continue;
}
showField(f.getName(), val, depth);
}
}
private String indent(int spaces) {
if (spaces == 0) {
return "";
} else {
return String.format("%" + spaces + "s", " ");
}
}
private void showField(String field, Object value, int depth) {
final int spacesDepthRatio = 2;
String indent = indent(depth * spacesDepthRatio);
out.print(indent);
out.print(field);
out.print(':');
if (value instanceof String && ((String) value).contains("\n")) {
out.print(' ');
// Idention for multi-line text is
// current depth indetion + length of field + length of ": "
indent = indent(indent.length() + field.length() + spacesDepthRatio);
out.print(((String) value).replaceAll("\n", "\n" + indent).trim());
out.print('\n');
} else if (value instanceof Long && isDateField(field)) {
out.print(' ');
out.print(dtf.print(((Long) value) * 1000L));
out.print('\n');
} else if (isPrimitive(value)) {
out.print(' ');
out.print(value);
out.print('\n');
} else if (value instanceof Collection) {
out.print('\n');
boolean firstElement = true;
for (Object thing : ((Collection<?>) value)) {
// The name of the collection was initially printed at the beginning
// of this routine. Beginning at the second sub-element, reprint
// the collection name so humans can separate individual elements
// with less strain and error.
//
if (firstElement) {
firstElement = false;
} else {
out.print(indent);
out.print(field);
out.print(":\n");
}
if (isPrimitive(thing)) {
out.print(' ');
out.print(value);
out.print('\n');
} else {
showText(thing, depth + 1);
}
}
} else {
out.print('\n');
showText(value, depth + 1);
}
}
private static boolean isPrimitive(Object value) {
return value instanceof String //
|| value instanceof Number //
|| value instanceof Boolean //
|| value instanceof Enum;
}
private static boolean isDateField(String name) {
return "lastUpdated".equals(name) //
|| "grantedOn".equals(name) //
|| "timestamp".equals(name) //
|| "createdOn".equals(name);
}
private List<Field> fieldsOf(Class<?> type) {
List<Field> r = new ArrayList<>();
if (type.getSuperclass() != null) {
r.addAll(fieldsOf(type.getSuperclass()));
}
r.addAll(Arrays.asList(type.getDeclaredFields()));
return r;
}
static class ErrorMessage {
public final String type = "error";
public String message;
}
}

View File

@@ -31,7 +31,6 @@ import com.google.inject.Provider;
import org.kohsuke.args4j.Option;
import java.util.BitSet;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
@@ -42,7 +41,6 @@ public class QueryChanges implements RestReadView<TopLevelResource> {
private final ChangeJson json;
private final QueryProcessor imp;
private final Provider<CurrentUser> user;
private boolean reverse;
private EnumSet<ListChangesOption> options;
@Option(name = "--query", aliases = {"-q"}, metaVar = "QUERY", usage = "Query string")
@@ -137,30 +135,13 @@ public class QueryChanges implements RestReadView<TopLevelResource> {
private List<List<ChangeInfo>> query0() throws OrmException,
QueryParseException {
int cnt = queries.size();
BitSet more = new BitSet(cnt);
List<List<ChangeData>> data = imp.queryChanges(queries);
for (int n = 0; n < cnt; n++) {
List<ChangeData> changes = data.get(n);
if (imp.getLimit() > 0 && changes.size() > imp.getLimit()) {
if (reverse) {
changes = changes.subList(1, changes.size());
} else {
changes = changes.subList(0, imp.getLimit());
}
data.set(n, changes);
more.set(n, true);
}
}
List<List<ChangeInfo>> res = json.addOptions(options).formatList2(data);
List<QueryResult> results = imp.queryByStrings(queries);
List<List<ChangeInfo>> res = json.addOptions(options)
.formatQueryResults(results);
for (int n = 0; n < cnt; n++) {
List<ChangeInfo> info = res.get(n);
if (more.get(n) && !info.isEmpty()) {
if (reverse) {
info.get(0)._moreChanges = true;
} else {
info.get(info.size() - 1)._moreChanges = true;
}
if (results.get(n).moreChanges()) {
info.get(info.size() - 1)._moreChanges = true;
}
}
return res;

View File

@@ -14,494 +14,178 @@
package com.google.gerrit.server.query.change;
import com.google.common.base.MoreObjects;
import static com.google.common.base.Preconditions.checkState;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.gerrit.common.TimeUtil;
import com.google.common.collect.Ordering;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.common.data.LabelTypes;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.config.TrackingFooters;
import com.google.gerrit.server.data.ChangeAttribute;
import com.google.gerrit.server.data.PatchSetAttribute;
import com.google.gerrit.server.data.QueryStatsAttribute;
import com.google.gerrit.server.events.EventFactory;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.project.SubmitRuleEvaluator;
import com.google.gerrit.server.index.IndexPredicate;
import com.google.gerrit.server.query.Predicate;
import com.google.gerrit.server.query.QueryParseException;
import com.google.gson.Gson;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.ResultSet;
import com.google.inject.Inject;
import org.eclipse.jgit.util.io.DisabledOutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.List;
public class QueryProcessor {
private static final Logger log =
LoggerFactory.getLogger(QueryProcessor.class);
public static enum OutputFormat {
TEXT, JSON
}
private final Gson gson = new Gson();
private final SimpleDateFormat sdf =
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss zzz");
private final EventFactory eventFactory;
private final ChangeQueryBuilder queryBuilder;
private final ChangeQueryRewriter queryRewriter;
private final TrackingFooters trackingFooters;
private final CurrentUser user;
private final int maxLimit;
private final int permittedLimit;
private OutputFormat outputFormat = OutputFormat.TEXT;
private int limit;
private int limitFromCaller;
private int start;
private boolean includePatchSets;
private boolean includeCurrentPatchSet;
private boolean includeApprovals;
private boolean includeComments;
private boolean includeFiles;
private boolean includeCommitMessage;
private boolean includeDependencies;
private boolean includeSubmitRecords;
private boolean includeAllReviewers;
private OutputStream outputStream = DisabledOutputStream.INSTANCE;
private PrintWriter out;
private boolean moreResults;
@Inject
QueryProcessor(EventFactory eventFactory,
ChangeQueryBuilder.Factory queryBuilder, CurrentUser currentUser,
ChangeQueryRewriter queryRewriter,
TrackingFooters trackingFooters) {
this.eventFactory = eventFactory;
QueryProcessor(ChangeQueryBuilder.Factory queryBuilder,
CurrentUser currentUser,
ChangeQueryRewriter queryRewriter) {
this.queryBuilder = queryBuilder.create(currentUser);
this.queryRewriter = queryRewriter;
this.trackingFooters = trackingFooters;
this.user = currentUser;
this.maxLimit = currentUser.getCapabilities()
this.permittedLimit = currentUser.getCapabilities()
.getRange(GlobalCapability.QUERY_LIMIT)
.getMax();
this.moreResults = false;
}
int getLimit() {
return limit;
public ChangeQueryBuilder getQueryBuilder() {
return queryBuilder;
}
void setLimit(int n) {
limit = n;
public void setLimit(int n) {
limitFromCaller = n;
}
public void setStart(int n) {
start = n;
}
public void setIncludePatchSets(boolean on) {
includePatchSets = on;
}
public boolean getIncludePatchSets() {
return includePatchSets;
}
public void setIncludeCurrentPatchSet(boolean on) {
includeCurrentPatchSet = on;
}
public boolean getIncludeCurrentPatchSet() {
return includeCurrentPatchSet;
}
public void setIncludeApprovals(boolean on) {
includeApprovals = on;
}
public void setIncludeComments(boolean on) {
includeComments = on;
}
public void setIncludeFiles(boolean on) {
includeFiles = on;
}
public boolean getIncludeFiles() {
return includeFiles;
}
public void setIncludeDependencies(boolean on) {
includeDependencies = on;
}
public boolean getIncludeDependencies() {
return includeDependencies;
}
public void setIncludeCommitMessage(boolean on) {
includeCommitMessage = on;
}
public void setIncludeSubmitRecords(boolean on) {
includeSubmitRecords = on;
}
public void setIncludeAllReviewers(boolean on) {
includeAllReviewers = on;
}
public void setOutput(OutputStream out, OutputFormat fmt) {
this.outputStream = out;
this.outputFormat = fmt;
/**
* Query for changes that match the query string.
*
* @see #queryChanges(List)
* @param queryString the query string to parse.
* @return results of the query.
*/
public QueryResult queryByString(String queryString)
throws OrmException, QueryParseException {
return queryByStrings(ImmutableList.of(queryString)).get(0);
}
/**
* Query for changes that match the query string.
* Perform multiple queries over a list of query strings.
*
* @see #queryChanges(List)
* @param queryStrings the query strings to parse.
* @return results of the queries, one list per input query.
*/
public List<QueryResult> queryByStrings(List<String> queryStrings)
throws OrmException, QueryParseException {
List<Predicate<ChangeData>> queries = new ArrayList<>(queryStrings.size());
for (String qs : queryStrings) {
queries.add(queryBuilder.parse(qs));
}
return queryChanges(queries);
}
/**
* Query for changes that match a structured query.
*
* @see #queryChanges(List)
* @param query the query.
* @return results of the query.
*/
public QueryResult queryChanges(Predicate<ChangeData> query)
throws OrmException, QueryParseException {
return queryChanges(ImmutableList.of(query)).get(0);
}
/*
* Perform multiple queries over a list of query strings.
* <p>
* If a limit was specified using {@link #setLimit(int)} this method may
* return up to {@code limit + 1} results, allowing the caller to determine if
* there are more than {@code limit} matches and suggest to its own caller
* that the query could be retried with {@link #setStart(int)}.
*
* @param queries the queries.
* @return results of the queries, one list per input query.
*/
public List<ChangeData> queryChanges(String queryString)
public List<QueryResult> queryChanges(List<Predicate<ChangeData>> queries)
throws OrmException, QueryParseException {
return queryChanges(ImmutableList.of(queryString)).get(0);
return queryChanges(null, queries);
}
/**
* Query for changes that match the query string.
* <p>
* If a limit was specified using {@link #setLimit(int)} this method may
* return up to {@code limit + 1} results, allowing the caller to determine if
* there are more than {@code limit} matches and suggest to its own caller
* that the query could be retried with {@link #setStart(int)}.
*/
public List<List<ChangeData>> queryChanges(List<String> queries)
static {
// In addition to this assumption, this queryChanges assumes the basic
// rewrites do not touch visibleto predicates either.
checkState(
!IsVisibleToPredicate.class.isAssignableFrom(IndexPredicate.class),
"QueryProcessor assumes visibleto is not used by the index rewriter.");
}
private List<QueryResult> queryChanges(List<String> queryStrings,
List<Predicate<ChangeData>> queries)
throws OrmException, QueryParseException {
final Predicate<ChangeData> visibleToMe = queryBuilder.is_visible();
Predicate<ChangeData> visibleToMe = queryBuilder.is_visible();
int cnt = queries.size();
// Parse and rewrite all queries.
List<Integer> limits = Lists.newArrayListWithCapacity(cnt);
List<ChangeDataSource> sources = Lists.newArrayListWithCapacity(cnt);
for (String query : queries) {
Predicate<ChangeData> q = parseQuery(query, visibleToMe);
Predicate<ChangeData> s = queryRewriter.rewrite(q, start);
List<Integer> limits = new ArrayList<>(cnt);
List<Predicate<ChangeData>> predicates = new ArrayList<>(cnt);
List<ChangeDataSource> sources = new ArrayList<>(cnt);
for (Predicate<ChangeData> q : queries) {
int limit = getEffectiveLimit(q);
limits.add(limit);
// Always bump limit by 1, even if this results in exceeding the permitted
// max for this user. The only way to see if there are more changes is to
// ask for one more result from the query.
Predicate<ChangeData> s = queryRewriter.rewrite(q, start, limit + 1);
if (!(s instanceof ChangeDataSource)) {
q = Predicate.and(queryBuilder.status_open(), q);
s = queryRewriter.rewrite(q, start);
s = queryRewriter.rewrite(q, start, limit);
}
if (!(s instanceof ChangeDataSource)) {
throw new QueryParseException("invalid query: " + s);
}
// Don't trust QueryRewriter to have left the visible predicate.
AndSource a = new AndSource(ImmutableList.of(s, visibleToMe), start);
limits.add(limit(q));
predicates.add(a);
sources.add(a);
}
// Run each query asynchronously, if supported.
List<ResultSet<ChangeData>> matches = Lists.newArrayListWithCapacity(cnt);
List<ResultSet<ChangeData>> matches = new ArrayList<>(cnt);
for (ChangeDataSource s : sources) {
matches.add(s.read());
}
List<List<ChangeData>> out = Lists.newArrayListWithCapacity(cnt);
List<QueryResult> out = new ArrayList<>(cnt);
for (int i = 0; i < cnt; i++) {
List<ChangeData> results = matches.get(i).toList();
if (results.size() > maxLimit) {
moreResults = true;
}
int limit = limits.get(i);
if (limit < results.size()) {
results = results.subList(0, limit);
}
out.add(results);
out.add(QueryResult.create(
queryStrings != null ? queryStrings.get(i) : null,
predicates.get(i),
limits.get(i),
matches.get(i).toList()));
}
return out;
}
public void query(String queryString) throws IOException {
out = new PrintWriter( //
new BufferedWriter( //
new OutputStreamWriter(outputStream, "UTF-8")));
try {
if (isDisabled()) {
ErrorMessage m = new ErrorMessage();
m.message = "query disabled";
show(m);
return;
}
try {
final QueryStatsAttribute stats = new QueryStatsAttribute();
stats.runTimeMilliseconds = TimeUtil.nowMs();
List<ChangeData> results = queryChanges(queryString);
ChangeAttribute c = null;
for (ChangeData d : results) {
ChangeControl cc = d.changeControl().forUser(user);
LabelTypes labelTypes = cc.getLabelTypes();
c = eventFactory.asChangeAttribute(d.change());
eventFactory.extend(c, d.change());
if (!trackingFooters.isEmpty()) {
eventFactory.addTrackingIds(c,
trackingFooters.extract(d.commitFooters()));
}
if (includeAllReviewers) {
eventFactory.addAllReviewers(c, d.notes());
}
if (includeSubmitRecords) {
eventFactory.addSubmitRecords(c, new SubmitRuleEvaluator(d)
.setAllowClosed(true)
.setAllowDraft(true)
.canSubmit());
}
if (includeCommitMessage) {
eventFactory.addCommitMessage(c, d.commitMessage());
}
if (includePatchSets) {
if (includeFiles) {
eventFactory.addPatchSets(c, d.patches(),
includeApprovals ? d.approvals().asMap() : null,
includeFiles, d.change(), labelTypes);
} else {
eventFactory.addPatchSets(c, d.patches(),
includeApprovals ? d.approvals().asMap() : null,
labelTypes);
}
}
if (includeCurrentPatchSet) {
PatchSet current = d.currentPatchSet();
if (current != null) {
c.currentPatchSet = eventFactory.asPatchSetAttribute(current);
eventFactory.addApprovals(c.currentPatchSet,
d.currentApprovals(), labelTypes);
if (includeFiles) {
eventFactory.addPatchSetFileNames(c.currentPatchSet,
d.change(), d.currentPatchSet());
}
}
}
if (includeComments) {
eventFactory.addComments(c, d.messages());
if (includePatchSets) {
for (PatchSetAttribute attribute : c.patchSets) {
eventFactory.addPatchSetComments(attribute, d.publishedComments());
}
}
}
if (includeDependencies) {
eventFactory.addDependencies(c, d.change());
}
show(c);
}
stats.rowCount = results.size();
if (moreResults) {
stats.resumeSortKey = c.sortKey;
}
stats.runTimeMilliseconds =
TimeUtil.nowMs() - stats.runTimeMilliseconds;
show(stats);
} catch (OrmException err) {
log.error("Cannot execute query: " + queryString, err);
ErrorMessage m = new ErrorMessage();
m.message = "cannot query database";
show(m);
} catch (QueryParseException e) {
ErrorMessage m = new ErrorMessage();
m.message = e.getMessage();
show(m);
} catch (NoSuchChangeException e) {
log.error("Missing change: " + e.getMessage(), e);
ErrorMessage m = new ErrorMessage();
m.message = "missing change " + e.getMessage();
show(m);
}
} finally {
try {
out.flush();
} finally {
out = null;
}
}
}
boolean isDisabled() {
return maxLimit <= 0;
return permittedLimit <= 0;
}
private int limit(Predicate<ChangeData> s) {
int n = MoreObjects.firstNonNull(ChangeQueryBuilder.getLimit(s), maxLimit);
return limit > 0 ? Math.min(n, limit) + 1 : n + 1;
}
private Predicate<ChangeData> parseQuery(String queryString,
final Predicate<ChangeData> visibleToMe) throws QueryParseException {
return Predicate.and(queryBuilder.parse(queryString),
queryBuilder.limit(limit > 0 ? Math.min(limit, maxLimit) + 1 : maxLimit),
visibleToMe);
}
private void show(Object data) {
switch (outputFormat) {
default:
case TEXT:
if (data instanceof ChangeAttribute) {
out.print("change ");
out.print(((ChangeAttribute) data).id);
out.print("\n");
showText(data, 1);
} else {
showText(data, 0);
}
out.print('\n');
break;
case JSON:
out.print(gson.toJson(data));
out.print('\n');
break;
private int getEffectiveLimit(Predicate<ChangeData> p) {
List<Integer> possibleLimits = new ArrayList<>(3);
possibleLimits.add(permittedLimit);
if (limitFromCaller > 0) {
possibleLimits.add(limitFromCaller);
}
}
private void showText(Object data, int depth) {
for (Field f : fieldsOf(data.getClass())) {
Object val;
try {
val = f.get(data);
} catch (IllegalArgumentException err) {
continue;
} catch (IllegalAccessException err) {
continue;
}
if (val == null) {
continue;
}
showField(f.getName(), val, depth);
Integer limitFromPredicate = LimitPredicate.getLimit(p);
if (limitFromPredicate != null) {
possibleLimits.add(limitFromPredicate);
}
}
private String indent(int spaces) {
if (spaces == 0) {
return "";
} else {
return String.format("%" + spaces + "s", " ");
}
}
private void showField(String field, Object value, int depth) {
final int spacesDepthRatio = 2;
String indent = indent(depth * spacesDepthRatio);
out.print(indent);
out.print(field);
out.print(':');
if (value instanceof String && ((String) value).contains("\n")) {
out.print(' ');
// Idention for multi-line text is
// current depth indetion + length of field + length of ": "
indent = indent(indent.length() + field.length() + spacesDepthRatio);
out.print(((String) value).replaceAll("\n", "\n" + indent).trim());
out.print('\n');
} else if (value instanceof Long && isDateField(field)) {
out.print(' ');
out.print(sdf.format(new Date(((Long) value) * 1000L)));
out.print('\n');
} else if (isPrimitive(value)) {
out.print(' ');
out.print(value);
out.print('\n');
} else if (value instanceof Collection) {
out.print('\n');
boolean firstElement = true;
for (Object thing : ((Collection<?>) value)) {
// The name of the collection was initially printed at the beginning
// of this routine. Beginning at the second sub-element, reprint
// the collection name so humans can separate individual elements
// with less strain and error.
//
if (firstElement) {
firstElement = false;
} else {
out.print(indent);
out.print(field);
out.print(":\n");
}
if (isPrimitive(thing)) {
out.print(' ');
out.print(value);
out.print('\n');
} else {
showText(thing, depth + 1);
}
}
} else {
out.print('\n');
showText(value, depth + 1);
}
}
private static boolean isPrimitive(Object value) {
return value instanceof String //
|| value instanceof Number //
|| value instanceof Boolean //
|| value instanceof Enum;
}
private static boolean isDateField(String name) {
return "lastUpdated".equals(name) //
|| "grantedOn".equals(name) //
|| "timestamp".equals(name) //
|| "createdOn".equals(name);
}
private List<Field> fieldsOf(Class<?> type) {
List<Field> r = new ArrayList<>();
if (type.getSuperclass() != null) {
r.addAll(fieldsOf(type.getSuperclass()));
}
r.addAll(Arrays.asList(type.getDeclaredFields()));
return r;
}
static class ErrorMessage {
public final String type = "error";
public String message;
return Ordering.natural().min(possibleLimits);
}
}

View File

@@ -0,0 +1,59 @@
// Copyright (C) 2014 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.auto.value.AutoValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.server.query.Predicate;
import java.util.List;
/** Results of a query over changes. */
@AutoValue
public abstract class QueryResult {
static QueryResult create(@Nullable String query,
Predicate<ChangeData> predicate, int limit, List<ChangeData> changes) {
boolean moreChanges;
if (changes.size() > limit) {
moreChanges = true;
changes = changes.subList(0, limit);
} else {
moreChanges = false;
}
return new AutoValue_QueryResult(query, predicate, changes, moreChanges);
}
/**
* @return the original query string, or null if the query was created
* programmatically.
*/
@Nullable public abstract String query();
/**
* @return the predicate after all rewriting and other modification by the
* query subsystem.
*/
public abstract Predicate<ChangeData> predicate();
/** @return the query results. */
public abstract List<ChangeData> changes();
/**
* @return whether the query could be retried with
* {@link QueryProcessor#setStart(int)} to produce more results. Never
* true if {@link #changes()} is empty.
*/
public abstract boolean moreChanges();
}

View File

@@ -32,7 +32,7 @@ import java.util.List;
/** A version of the database schema. */
public abstract class SchemaVersion {
/** The current schema version. */
public static final Class<Schema_101> C = Schema_101.class;
public static final Class<Schema_102> C = Schema_102.class;
public static class Module extends AbstractModule {
@Override

View File

@@ -0,0 +1,51 @@
// Copyright (C) 2014 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.schema;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gwtorm.jdbc.JdbcSchema;
import com.google.gwtorm.schema.sql.DialectPostgreSQL;
import com.google.gwtorm.schema.sql.SqlDialect;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.sql.SQLException;
import java.sql.Statement;
public class Schema_102 extends SchemaVersion {
@Inject
Schema_102(Provider<Schema_100> prior) {
super(prior);
}
@Override
protected void migrateData(ReviewDb db, UpdateUI ui)
throws OrmException, SQLException {
JdbcSchema schema = (JdbcSchema) db;
SqlDialect dialect = schema.getDialect();
try (Statement stmt = schema.getConnection().createStatement()) {
stmt.executeUpdate("DROP INDEX changes_byProjectOpen");
if (dialect instanceof DialectPostgreSQL) {
stmt.executeUpdate("CREATE INDEX changes_byProjectOpen"
+ " ON changes (dest_project_name, last_updated_on)"
+ " WHERE open = 'Y'");
} else {
stmt.executeUpdate("CREATE INDEX changes_byProjectOpen"
+ " ON changes (open, dest_project_name, last_updated_on)");
}
}
}
}

View File

@@ -34,7 +34,6 @@ import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.PatchSetApproval.LabelId;
import com.google.gerrit.reviewdb.client.PatchSetInfo;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.AuthRequest;
@@ -112,7 +111,6 @@ public class LabelNormalizerTest {
new Change.Id(1), userId,
new Branch.NameKey(allProjects, "refs/heads/master"),
TimeUtil.nowTs());
ChangeUtil.computeSortKey(change);
PatchSetInfo ps = new PatchSetInfo(new PatchSet.Id(change.getId(), 1));
ps.setSubject("Test change");
change.setCurrentPatchSet(ps);

View File

@@ -97,7 +97,7 @@ public class IndexRewriteTest {
parse("-status:abandoned (status:open OR status:merged)");
assertEquals(
query(parse("status:new OR status:submitted OR status:draft OR status:merged")),
rewrite.rewrite(in, 0));
rewrite.rewrite(in, 0, DEFAULT_MAX_QUERY_LIMIT));
}
@Test
@@ -158,24 +158,26 @@ public class IndexRewriteTest {
}
@Test
public void testLimit() throws Exception {
Predicate<ChangeData> in = parse("file:a limit:3");
Predicate<ChangeData> out = rewrite(in);
public void testLimitArgumentOverridesAllLimitPredicates() throws Exception {
Predicate<ChangeData> in = parse("limit:1 file:a limit:3");
Predicate<ChangeData> out = rewrite(in, 5);
assertSame(AndSource.class, out.getClass());
assertEquals(ImmutableList.of(
query(in.getChild(0), 3),
in.getChild(1)),
query(in.getChild(1), 5),
parse("limit:5"),
parse("limit:5")),
out.getChildren());
}
@Test
public void testStartIncreasesLimit() throws Exception {
int n = 3;
Predicate<ChangeData> f = parse("file:a");
Predicate<ChangeData> l = parse("limit:3");
Predicate<ChangeData> l = parse("limit:" + n);
Predicate<ChangeData> in = and(f, l);
assertEquals(and(query(f, 3), l), rewrite.rewrite(in, 0));
assertEquals(and(query(f, 4), l), rewrite.rewrite(in, 1));
assertEquals(and(query(f, 5), l), rewrite.rewrite(in, 2));
assertEquals(and(query(f, 3), parse("limit:3")), rewrite.rewrite(in, 0, n));
assertEquals(and(query(f, 4), parse("limit:4")), rewrite.rewrite(in, 1, n));
assertEquals(and(query(f, 5), parse("limit:5")), rewrite.rewrite(in, 2, n));
}
@Test
@@ -220,7 +222,12 @@ public class IndexRewriteTest {
private Predicate<ChangeData> rewrite(Predicate<ChangeData> in)
throws QueryParseException {
return rewrite.rewrite(in, 0);
return rewrite.rewrite(in, 0, DEFAULT_MAX_QUERY_LIMIT);
}
private Predicate<ChangeData> rewrite(Predicate<ChangeData> in, int limit)
throws QueryParseException {
return rewrite.rewrite(in, 0, limit);
}
private IndexedChangeQuery query(Predicate<ChangeData> p)

View File

@@ -136,18 +136,22 @@ public abstract class AbstractQueryChangesTest {
userAccount.setPreferredEmail("user@example.com");
db.accounts().update(ImmutableList.of(userAccount));
user = userFactory.create(userId);
requestContext.setContext(newRequestContext(userAccount.getId()));
}
requestContext.setContext(new RequestContext() {
private RequestContext newRequestContext(Account.Id requestUserId) {
final CurrentUser requestUser = userFactory.create(requestUserId);
return new RequestContext() {
@Override
public CurrentUser getCurrentUser() {
return user;
return requestUser;
}
@Override
public Provider<ReviewDb> getReviewDbProvider() {
return Providers.of(db);
}
});
};
}
@After
@@ -543,9 +547,22 @@ public abstract class AbstractQueryChangesTest {
List<ChangeInfo> results;
for (int i = 1; i <= n + 2; i++) {
int expectedSize;
Boolean expectedMoreChanges;
if (i < n) {
expectedSize = i;
expectedMoreChanges = true;
} else {
expectedSize = n;
expectedMoreChanges = null;
}
results = query("status:new limit:" + i);
assertThat(results).hasSize(Math.min(i, n));
String msg = "i=" + i;
assert_().withFailureMessage(msg).that(results).hasSize(expectedSize);
assertResultEquals(last, results.get(0));
assert_().withFailureMessage(msg)
.that(results.get(results.size() - 1)._moreChanges)
.isEqualTo(expectedMoreChanges);
}
}
@@ -1031,6 +1048,49 @@ public abstract class AbstractQueryChangesTest {
assertThat(query("repo")).hasSize(6);
}
@Test
public void implicitVisibleTo() throws Exception {
TestRepository<InMemoryRepository> repo = createProject("repo");
Change change1 = newChange(repo, null, null, userId.get(), null).insert();
ChangeInserter ins2 = newChange(repo, null, null, userId.get(), null);
Change change2 = ins2.getChange();
change2.setStatus(Change.Status.DRAFT);
ins2.insert();
String q = "project:repo";
List<ChangeInfo> results = query(q);
assertThat(results).hasSize(2);
assertResultEquals(change2, results.get(0));
assertResultEquals(change1, results.get(1));
// Second user cannot see first user's drafts.
requestContext.setContext(newRequestContext(accountManager
.authenticate(AuthRequest.forUser("anotheruser")).getAccountId()));
assertResultEquals(change1, queryOne(q));
}
@Test
public void explicitVisibleTo() throws Exception {
TestRepository<InMemoryRepository> repo = createProject("repo");
Change change1 = newChange(repo, null, null, userId.get(), null).insert();
ChangeInserter ins2 = newChange(repo, null, null, userId.get(), null);
Change change2 = ins2.getChange();
change2.setStatus(Change.Status.DRAFT);
ins2.insert();
String q = "project:repo";
List<ChangeInfo> results = query(q);
assertThat(results).hasSize(2);
assertResultEquals(change2, results.get(0));
assertResultEquals(change1, results.get(1));
// Second user cannot see first user's drafts.
Account.Id user2 = accountManager
.authenticate(AuthRequest.forUser("anotheruser"))
.getAccountId();
assertResultEquals(change1, queryOne(q + " visibleto:" + user2.get()));
}
protected ChangeInserter newChange(
TestRepository<InMemoryRepository> repo,
@Nullable RevCommit commit, @Nullable String key, @Nullable Integer owner,

View File

@@ -25,7 +25,6 @@ import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetInfo;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.AllUsersNameProvider;
@@ -114,6 +113,5 @@ public class TestChanges {
change.getId(), curr != null ? curr.get() + 1 : 1));
ps.setSubject("Change subject");
change.setCurrentPatchSet(ps);
ChangeUtil.computeSortKey(change);
}
}

View File

@@ -14,7 +14,8 @@
package com.google.gerrit.sshd.commands;
import com.google.gerrit.server.query.change.QueryProcessor;
import com.google.gerrit.server.query.change.OutputStreamQuery;
import com.google.gerrit.server.query.change.OutputStreamQuery.OutputFormat;
import com.google.gerrit.sshd.CommandMetaData;
import com.google.gerrit.sshd.SshCommand;
import com.google.inject.Inject;
@@ -27,10 +28,10 @@ import java.util.List;
@CommandMetaData(name = "query", description = "Query the change database")
class Query extends SshCommand {
@Inject
private QueryProcessor processor;
private OutputStreamQuery processor;
@Option(name = "--format", metaVar = "FMT", usage = "Output display format")
void setFormat(QueryProcessor.OutputFormat format) {
void setFormat(OutputFormat format) {
processor.setOutput(out, format);
}
@@ -97,7 +98,7 @@ class Query extends SshCommand {
@Override
protected void parseCommandLine() throws UnloggedFailure {
processor.setOutput(out, QueryProcessor.OutputFormat.TEXT);
processor.setOutput(out, OutputFormat.TEXT);
super.parseCommandLine();
if (processor.getIncludeFiles() &&
!(processor.getIncludePatchSets() || processor.getIncludeCurrentPatchSet())) {