Merge branch 'stable-2.14'

* stable-2.14:
  Move systemctl files to /init/ rather than /systemd/
  SitePathInitializer: Fix destination file name for gerrit.socket
  Clarify documentation for accountPatchReviewDb.url
  Add the new gerrit systemctl file to init
  Remove unneeded output in MigrateAccountPatchReviewDb
  ES: Implement online reindex for ElasticSearch
  JdbcAccountPatchReviewStore: Fix copyright year
  Fix documentation nits in pgm-MigrateAccountPatchReviewDb.txt
  Support Jdbc implementation of AccountPatchReviewStore
  ES: Temporarily disable server discovery

Change-Id: I1034869bc6146db929f6242610c9eb13a7b092f7
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 1e87c5b..8e1c81d 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -19,6 +19,26 @@
   directory = /var/cache/gerrit2
 ----
 
+[[accountPatchReviewDb]]
+=== Section accountPatchReviewDb
+
+[[accountPatchReviewDb.url]]accountPatchReviewDb.url::
++
+The url of accountPatchReviewDb. Supported types are `H2`, `POSTGRESQL`, and
+`MYSQL`. Drop the driver jar in the lib folder of the site path if the Jdbc
+driver of the corresponding Database is not yet in the class path.
++
+Default is to create H2 database in the db folder of the site path.
++
+Changing this parameter requires to migrate database using the
+link:pgm-MigrateAccountPatchReviewDb.html[MigrateAccountPatchReviewDb] program.
+Migration cannot be done while the server is running.
+
+----
+[accountPatchReviewDb]
+  url = jdbc:postgresql://<host>:<port>/<db_name>?user=<user>&password=<password>
+----
+
 [[accounts]]
 === Section accounts
 
diff --git a/Documentation/pgm-MigrateAccountPatchReviewDb.txt b/Documentation/pgm-MigrateAccountPatchReviewDb.txt
new file mode 100644
index 0000000..a2cb53e
--- /dev/null
+++ b/Documentation/pgm-MigrateAccountPatchReviewDb.txt
@@ -0,0 +1,62 @@
+= MigrateAccountPatchReviewDb
+
+== NAME
+MigrateAccountPatchReviewDb - Migrates account patch review db from one database
+backend to another.
+
+== SYNOPSIS
+[verse]
+--
+_java_ -jar gerrit.war MigrateAccountPatchReviewDb
+  -d <SITE_PATH>
+  [--sourceUrl] [--chunkSize]
+--
+
+== DESCRIPTION
+Migrates AccountPatchReviewDb from one database backend to another. The
+AccountPatchReviewDb is a database used to store the user file reviewed flags.
+
+This command is only intended to be run if the configuration parameter
+link:config-gerrit.html#accountPatchReviewDb.url[accountPatchReviewDb.url]
+is set or changed.
+
+To migrate AccountPatchReviewDb:
+
+* Stop Gerrit
+* Configure new value for link:config-gerrit.html#accountPatchReviewDb.url[accountPatchReviewDb.url]
+* Migrate data using this command
+* Start Gerrit
+
+== OPTIONS
+
+-d::
+--sourceUrl::
+	Url of source database. Only need to be specified if the source is not H2.
+
+--chunkSize::
+	Chunk size of fetching from source and pushing to target on each time.
+	Defaults to 100000.
+
+== CONTEXT
+This command can only be run on a server which has direct
+connectivity to the database.
+
+== EXAMPLES
+To migrate from H2 to the database specified by
+link:config-gerrit.html#accountPatchReviewDb.url[accountPatchReviewDb.url]
+in gerrit.config:
+
+----
+	$ java -jar gerrit.war MigrateAccountPatchReviewDb
+----
+
+== SEE ALSO
+
+* Configuration parameter link:config-gerrit.html#accountPatchReviewDb.url[accountPatchReviewDb.url]
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/pgm-index.txt b/Documentation/pgm-index.txt
index 0c347f4..d61cc0b 100644
--- a/Documentation/pgm-index.txt
+++ b/Documentation/pgm-index.txt
@@ -41,6 +41,9 @@
 link:pgm-LocalUsernamesToLowerCase.html[LocalUsernamesToLowerCase]::
 	Convert the local username of every account to lower case.
 
+link:pgm-MigrateAccountPatchReviewDb.html[MigrateAccountPatchReviewDb]::
+	Migrates AccountPatchReviewDb from one database backend to another.
+
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java
index 53e7c19..2108815 100644
--- a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java
+++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java
@@ -62,7 +62,11 @@
             .build(GroupIndex.Factory.class));
 
     install(new IndexModule(threads));
-    install(new SingleVersionModule(singleVersions));
+    if (singleVersions == null) {
+      listener().to(ElasticVersionManager.class);
+    } else {
+      install(new SingleVersionModule(singleVersions));
+    }
   }
 
   @Provides
diff --git a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticIndexVersionDiscovery.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticIndexVersionDiscovery.java
new file mode 100644
index 0000000..b73b37f
--- /dev/null
+++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticIndexVersionDiscovery.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2017 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.elasticsearch;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import io.searchbox.client.JestResult;
+import io.searchbox.client.http.JestHttpClient;
+import io.searchbox.indices.aliases.GetAliases;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map.Entry;
+
+@Singleton
+class ElasticIndexVersionDiscovery {
+  private final JestHttpClient client;
+
+  @Inject
+  ElasticIndexVersionDiscovery(JestClientBuilder clientBuilder) {
+    this.client = clientBuilder.build();
+  }
+
+  List<String> discover(String prefix, String indexName) throws IOException {
+    String name = prefix + indexName + "_";
+    JestResult result = client.execute(new GetAliases.Builder().addIndex(name + "*").build());
+    if (result.isSucceeded()) {
+      JsonObject object = result.getJsonObject().getAsJsonObject();
+      List<String> versions = new ArrayList<>(object.size());
+      for (Entry<String, JsonElement> entry : object.entrySet()) {
+        versions.add(entry.getKey().replace(name, ""));
+      }
+      return versions;
+    }
+    return Collections.emptyList();
+  }
+}
diff --git a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticVersionManager.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticVersionManager.java
new file mode 100644
index 0000000..917217a
--- /dev/null
+++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticVersionManager.java
@@ -0,0 +1,251 @@
+// Copyright (C) 2017 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.elasticsearch;
+
+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.Maps;
+import com.google.common.primitives.Ints;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.index.Index;
+import com.google.gerrit.server.index.IndexCollection;
+import com.google.gerrit.server.index.IndexDefinition;
+import com.google.gerrit.server.index.IndexDefinition.IndexFactory;
+import com.google.gerrit.server.index.IndexUtils;
+import com.google.gerrit.server.index.OnlineReindexer;
+import com.google.gerrit.server.index.ReindexerAlreadyRunningException;
+import com.google.gerrit.server.index.Schema;
+import com.google.inject.Inject;
+import com.google.inject.ProvisionException;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+public class ElasticVersionManager implements LifecycleListener {
+  private static final Logger log = LoggerFactory.getLogger(ElasticVersionManager.class);
+
+  private static class Version<V> {
+    private final Schema<V> schema;
+    private final int version;
+    private final boolean ready;
+
+    private Version(Schema<V> schema, int version, boolean ready) {
+      checkArgument(schema == null || schema.getVersion() == version);
+      this.schema = schema;
+      this.version = version;
+      this.ready = ready;
+    }
+  }
+
+  private final Map<String, IndexDefinition<?, ?, ?>> defs;
+  private final Map<String, OnlineReindexer<?, ?, ?>> reindexers;
+  private final ElasticIndexVersionDiscovery versionDiscovery;
+  private final SitePaths sitePaths;
+  private final boolean onlineUpgrade;
+  private final String runReindexMsg;
+  private final String prefix;
+
+  @Inject
+  ElasticVersionManager(
+      @GerritServerConfig Config cfg,
+      SitePaths sitePaths,
+      Collection<IndexDefinition<?, ?, ?>> defs,
+      ElasticIndexVersionDiscovery versionDiscovery) {
+    this.sitePaths = sitePaths;
+    this.versionDiscovery = versionDiscovery;
+    this.defs = Maps.newHashMapWithExpectedSize(defs.size());
+    for (IndexDefinition<?, ?, ?> def : defs) {
+      this.defs.put(def.getName(), def);
+    }
+
+    prefix = MoreObjects.firstNonNull(cfg.getString("index", null, "prefix"), "gerrit");
+    reindexers = Maps.newHashMapWithExpectedSize(defs.size());
+    onlineUpgrade = cfg.getBoolean("index", null, "onlineUpgrade", true);
+    runReindexMsg =
+        "No index versions ready; run java -jar "
+            + sitePaths.gerrit_war.toAbsolutePath()
+            + " reindex";
+  }
+
+  @Override
+  public void start() {
+    try {
+      for (IndexDefinition<?, ?, ?> def : defs.values()) {
+        initIndex(def);
+      }
+    } catch (IOException e) {
+      ProvisionException ex = new ProvisionException("Error scanning indexes");
+      ex.initCause(e);
+      throw ex;
+    }
+  }
+
+  private <K, V, I extends Index<K, V>> void initIndex(IndexDefinition<K, V, I> def)
+      throws IOException {
+    TreeMap<Integer, Version<V>> versions = scanVersions(def);
+    // Search from the most recent ready version.
+    // Write to the most recent ready version and the most recent version.
+    Version<V> search = null;
+    List<Version<V>> write = Lists.newArrayListWithCapacity(2);
+    for (Version<V> v : versions.descendingMap().values()) {
+      if (v.schema == null) {
+        continue;
+      }
+      if (write.isEmpty() && onlineUpgrade) {
+        write.add(v);
+      }
+      if (v.ready) {
+        search = v;
+        if (!write.contains(v)) {
+          write.add(v);
+        }
+        break;
+      }
+    }
+    if (search == null) {
+      throw new ProvisionException(runReindexMsg);
+    }
+
+    IndexFactory<K, V, I> factory = def.getIndexFactory();
+    I searchIndex = factory.create(search.schema);
+    IndexCollection<K, V, I> indexes = def.getIndexCollection();
+    indexes.setSearchIndex(searchIndex);
+    for (Version<V> v : write) {
+      if (v.schema != null) {
+        if (v.version != search.version) {
+          indexes.addWriteIndex(factory.create(v.schema));
+        } else {
+          indexes.addWriteIndex(searchIndex);
+        }
+      }
+    }
+
+    markNotReady(def.getName(), versions.values(), write);
+
+    synchronized (this) {
+      if (!reindexers.containsKey(def.getName())) {
+        int latest = write.get(0).version;
+        OnlineReindexer<K, V, I> reindexer = new OnlineReindexer<>(def, latest);
+        reindexers.put(def.getName(), reindexer);
+        if (onlineUpgrade && latest != search.version) {
+          reindexer.start();
+        }
+      }
+    }
+  }
+
+  /**
+   * Start the online reindexer if the current index is not already the latest.
+   *
+   * @param name index name
+   * @param force start re-index
+   * @return true if started, otherwise false.
+   * @throws ReindexerAlreadyRunningException
+   */
+  public synchronized boolean startReindexer(String name, boolean force)
+      throws ReindexerAlreadyRunningException {
+    OnlineReindexer<?, ?, ?> reindexer = reindexers.get(name);
+    validateReindexerNotRunning(reindexer);
+    if (force || !isLatestIndexVersion(name, reindexer)) {
+      reindexer.start();
+      return true;
+    }
+    return false;
+  }
+
+  /**
+   * Activate the latest index if the current index is not already the latest.
+   *
+   * @param name index name
+   * @return true if index was activated, otherwise false.
+   * @throws ReindexerAlreadyRunningException
+   */
+  public synchronized boolean activateLatestIndex(String name)
+      throws ReindexerAlreadyRunningException {
+    OnlineReindexer<?, ?, ?> reindexer = reindexers.get(name);
+    validateReindexerNotRunning(reindexer);
+    if (!isLatestIndexVersion(name, reindexer)) {
+      reindexer.activateIndex();
+      return true;
+    }
+    return false;
+  }
+
+  private boolean isLatestIndexVersion(String name, OnlineReindexer<?, ?, ?> reindexer) {
+    int readVersion = defs.get(name).getIndexCollection().getSearchIndex().getSchema().getVersion();
+    return reindexer == null || reindexer.getVersion() == readVersion;
+  }
+
+  private static void validateReindexerNotRunning(OnlineReindexer<?, ?, ?> reindexer)
+      throws ReindexerAlreadyRunningException {
+    if (reindexer != null && reindexer.isRunning()) {
+      throw new ReindexerAlreadyRunningException();
+    }
+  }
+
+  private <K, V, I extends Index<K, V>> TreeMap<Integer, Version<V>> scanVersions(
+      IndexDefinition<K, V, I> def) throws IOException {
+    TreeMap<Integer, Version<V>> versions = new TreeMap<>();
+    for (Schema<V> schema : def.getSchemas().values()) {
+      int v = schema.getVersion();
+      versions.put(
+          v,
+          new Version<>(
+              schema, v, IndexUtils.getReady(sitePaths, def.getName(), schema.getVersion())));
+    }
+
+    try {
+      for (String version : versionDiscovery.discover(prefix, def.getName())) {
+        Integer v = Ints.tryParse(version);
+        if (v == null || version.length() != 4) {
+          log.warn("Unrecognized version in index {}: {}", def.getName(), version);
+          continue;
+        }
+        if (!versions.containsKey(v)) {
+          versions.put(
+              v, new Version<V>(null, v, IndexUtils.getReady(sitePaths, def.getName(), v)));
+        }
+      }
+    } catch (IOException e) {
+      log.error("Error scanning index: " + def.getName(), e);
+    }
+    return versions;
+  }
+
+  private <V> void markNotReady(
+      String name, Iterable<Version<V>> versions, Collection<Version<V>> inUse) throws IOException {
+    for (Version<V> v : versions) {
+      if (!inUse.contains(v)) {
+        IndexUtils.getReady(sitePaths, name, v.version);
+      }
+    }
+  }
+
+  @Override
+  public void stop() {
+    // Do nothing; indexes are closed on demand by IndexCollection.
+  }
+}
diff --git a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/JestClientBuilder.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/JestClientBuilder.java
index 330c250..65949b7 100644
--- a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/JestClientBuilder.java
+++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/JestClientBuilder.java
@@ -60,7 +60,9 @@
     factory.setHttpClientConfig(
         new HttpClientConfig.Builder(url)
             .multiThreaded(true)
-            .discoveryEnabled(!refresh)
+            // Temporary disable servers discovery.
+            // We can enable it again when we can wait for it to finish
+            .discoveryEnabled(false)
             .discoveryFrequency(1L, TimeUnit.MINUTES)
             .build());
     return (JestHttpClient) factory.getObject();
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
index 94b3618..a2d4f1e 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.server.index.IndexDefinition;
 import com.google.gerrit.server.index.IndexDefinition.IndexFactory;
 import com.google.gerrit.server.index.OnlineReindexer;
+import com.google.gerrit.server.index.ReindexerAlreadyRunningException;
 import com.google.gerrit.server.index.Schema;
 import com.google.inject.Inject;
 import com.google.inject.ProvisionException;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
index 475ff2b..268bb12 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
@@ -77,6 +77,7 @@
 import com.google.gerrit.server.plugins.PluginRestApiModule;
 import com.google.gerrit.server.schema.DataSourceProvider;
 import com.google.gerrit.server.schema.H2AccountPatchReviewStore;
+import com.google.gerrit.server.schema.JdbcAccountPatchReviewStore;
 import com.google.gerrit.server.schema.SchemaVersionCheck;
 import com.google.gerrit.server.securestore.DefaultSecureStore;
 import com.google.gerrit.server.securestore.SecureStore;
@@ -359,7 +360,7 @@
     modules.add(
         test
             ? new H2AccountPatchReviewStore.InMemoryModule()
-            : new H2AccountPatchReviewStore.Module());
+            : new JdbcAccountPatchReviewStore.Module(config));
     modules.add(new ReceiveCommitsExecutorModule());
     modules.add(new DiffExecutorModule());
     modules.add(new MimeUtil2Module());
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/MigrateAccountPatchReviewDb.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/MigrateAccountPatchReviewDb.java
new file mode 100644
index 0000000..5fd5a08
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/MigrateAccountPatchReviewDb.java
@@ -0,0 +1,151 @@
+// Copyright (C) 2017 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.pgm;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Strings;
+import com.google.gerrit.pgm.util.SiteProgram;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.schema.DataSourceProvider;
+import com.google.gerrit.server.schema.JdbcAccountPatchReviewStore;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.List;
+import org.eclipse.jgit.lib.Config;
+import org.kohsuke.args4j.Option;
+
+/** Migrates AccountPatchReviewDb from one to another */
+public class MigrateAccountPatchReviewDb extends SiteProgram {
+
+  @Option(name = "--sourceUrl", usage = "Url of source database")
+  private String sourceUrl;
+
+  @Option(
+    name = "--chunkSize",
+    usage = "chunk size of fetching from source and push to target on each time"
+  )
+  private static long chunkSize = 100000;
+
+  @Override
+  public int run() throws Exception {
+    SitePaths sitePaths = new SitePaths(getSitePath());
+    Config fakeCfg = new Config();
+    if (!Strings.isNullOrEmpty(sourceUrl)) {
+      fakeCfg.setString("accountPatchReviewDb", null, "url", sourceUrl);
+    }
+    JdbcAccountPatchReviewStore sourceJdbcAccountPatchReviewStore =
+        JdbcAccountPatchReviewStore.createAccountPatchReviewStore(fakeCfg, sitePaths);
+
+    Injector dbInjector = createDbInjector(DataSourceProvider.Context.SINGLE_USER);
+    Config cfg = dbInjector.getInstance(Key.get(Config.class, GerritServerConfig.class));
+    String targetUrl = cfg.getString("accountPatchReviewDb", null, "url");
+    if (targetUrl == null) {
+      System.err.println("accountPatchReviewDb.url is null in gerrit.config");
+      return 1;
+    }
+    System.out.println("target Url: " + targetUrl);
+    JdbcAccountPatchReviewStore targetJdbcAccountPatchReviewStore =
+        JdbcAccountPatchReviewStore.createAccountPatchReviewStore(cfg, sitePaths);
+    targetJdbcAccountPatchReviewStore.createTableIfNotExists();
+
+    if (!isTargetTableEmpty(targetJdbcAccountPatchReviewStore)) {
+      System.err.println("target table is not empty, cannot proceed");
+      return 1;
+    }
+
+    try (Connection sourceCon = sourceJdbcAccountPatchReviewStore.getConnection();
+        Connection targetCon = targetJdbcAccountPatchReviewStore.getConnection();
+        PreparedStatement sourceStmt =
+            sourceCon.prepareStatement(
+                "SELECT account_id, change_id, patch_set_id, file_name "
+                    + "FROM account_patch_reviews "
+                    + "LIMIT ? "
+                    + "OFFSET ?");
+        PreparedStatement targetStmt =
+            targetCon.prepareStatement(
+                "INSERT INTO account_patch_reviews "
+                    + "(account_id, change_id, patch_set_id, file_name) VALUES "
+                    + "(?, ?, ?, ?)")) {
+      targetCon.setAutoCommit(false);
+      long offset = 0;
+      List<Row> rows = selectRows(sourceStmt, offset);
+      while (!rows.isEmpty()) {
+        insertRows(targetCon, targetStmt, rows);
+        offset += rows.size();
+        rows = selectRows(sourceStmt, offset);
+      }
+    }
+    return 0;
+  }
+
+  @AutoValue
+  abstract static class Row {
+    abstract int accountId();
+
+    abstract int changeId();
+
+    abstract int patchSetId();
+
+    abstract String fileName();
+  }
+
+  private static boolean isTargetTableEmpty(JdbcAccountPatchReviewStore store) throws SQLException {
+    try (Connection con = store.getConnection();
+        Statement s = con.createStatement();
+        ResultSet r = s.executeQuery("SELECT COUNT(1) FROM account_patch_reviews")) {
+      if (r.next()) {
+        return r.getInt(1) == 0;
+      }
+      return true;
+    }
+  }
+
+  private static List<Row> selectRows(PreparedStatement stmt, long offset) throws SQLException {
+    List<Row> results = new ArrayList<>();
+    stmt.setLong(1, chunkSize);
+    stmt.setLong(2, offset);
+    try (ResultSet rs = stmt.executeQuery()) {
+      while (rs.next()) {
+        results.add(
+            new AutoValue_MigrateAccountPatchReviewDb_Row(
+                rs.getInt("account_id"),
+                rs.getInt("change_id"),
+                rs.getInt("patch_set_id"),
+                rs.getString("file_name")));
+      }
+    }
+    return results;
+  }
+
+  private static void insertRows(Connection con, PreparedStatement stmt, List<Row> rows)
+      throws SQLException {
+    for (Row r : rows) {
+      stmt.setLong(1, r.accountId());
+      stmt.setLong(2, r.changeId());
+      stmt.setLong(3, r.patchSetId());
+      stmt.setString(4, r.fileName());
+      stmt.addBatch();
+    }
+    stmt.executeBatch();
+    con.commit();
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
index 55bffaf..243ea09 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
@@ -100,7 +100,7 @@
     chmod(0755, site.gerrit_sh);
     extract(site.gerrit_service, getClass(), "gerrit.service");
     chmod(0755, site.gerrit_service);
-    extract(site.gerrit_service, getClass(), "gerrit.socket");
+    extract(site.gerrit_socket, getClass(), "gerrit.socket");
     chmod(0755, site.gerrit_socket);
     chmod(0700, site.tmp_dir);
 
diff --git a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/systemd/gerrit.service b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/gerrit.service
similarity index 100%
rename from gerrit-pgm/src/main/resources/com/google/gerrit/pgm/systemd/gerrit.service
rename to gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/gerrit.service
diff --git a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/systemd/gerrit.socket b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/gerrit.socket
similarity index 100%
rename from gerrit-pgm/src/main/resources/com/google/gerrit/pgm/systemd/gerrit.socket
rename to gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/gerrit.socket
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexUtils.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexUtils.java
index 708e010..7000e04 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexUtils.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexUtils.java
@@ -43,6 +43,15 @@
     }
   }
 
+  public static boolean getReady(SitePaths sitePaths, String name, int version) throws IOException {
+    try {
+      GerritIndexStatus cfg = new GerritIndexStatus(sitePaths);
+      return cfg.getReady(name, version);
+    } catch (ConfigInvalidException e) {
+      throw new IOException(e);
+    }
+  }
+
   public static Set<String> accountFields(QueryOptions opts) {
     Set<String> fs = opts.fields();
     return fs.contains(AccountField.ID.getName())
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/ReindexerAlreadyRunningException.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ReindexerAlreadyRunningException.java
similarity index 94%
rename from gerrit-lucene/src/main/java/com/google/gerrit/lucene/ReindexerAlreadyRunningException.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/index/ReindexerAlreadyRunningException.java
index 0ca632b..8bf99a5 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/ReindexerAlreadyRunningException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ReindexerAlreadyRunningException.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.lucene;
+package com.google.gerrit.server.index;
 
 public class ReindexerAlreadyRunningException extends Exception {
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/SingleVersionModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/SingleVersionModule.java
index 2df0b40..bf28d7d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/SingleVersionModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/SingleVersionModule.java
@@ -24,6 +24,7 @@
 import com.google.inject.TypeLiteral;
 import com.google.inject.name.Named;
 import com.google.inject.name.Names;
+import com.google.inject.util.Providers;
 import java.util.Collection;
 import java.util.Map;
 import java.util.Set;
@@ -44,7 +45,7 @@
     listener().to(SingleVersionListener.class);
     bind(new TypeLiteral<Map<String, Integer>>() {})
         .annotatedWith(Names.named(SINGLE_VERSIONS))
-        .toInstance(singleVersions);
+        .toProvider(Providers.of(singleVersions));
   }
 
   @Singleton
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/H2AccountPatchReviewStore.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/H2AccountPatchReviewStore.java
index 822ed6b..8585b47 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/H2AccountPatchReviewStore.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/H2AccountPatchReviewStore.java
@@ -15,13 +15,8 @@
 package com.google.gerrit.server.schema;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.primitives.Ints;
-import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.lifecycle.LifecycleModule;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.change.AccountPatchReviewStore;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
@@ -29,31 +24,11 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
-import java.sql.Connection;
-import java.sql.DriverManager;
-import java.sql.PreparedStatement;
-import java.sql.ResultSet;
 import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.Collection;
-import java.util.Optional;
-import javax.sql.DataSource;
-import org.apache.commons.dbcp.BasicDataSource;
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @Singleton
-public class H2AccountPatchReviewStore implements AccountPatchReviewStore, LifecycleListener {
-  private static final Logger log = LoggerFactory.getLogger(H2AccountPatchReviewStore.class);
-
-  public static class Module extends LifecycleModule {
-    @Override
-    protected void configure() {
-      DynamicItem.bind(binder(), AccountPatchReviewStore.class).to(H2AccountPatchReviewStore.class);
-      listener().to(H2AccountPatchReviewStore.class);
-    }
-  }
+public class H2AccountPatchReviewStore extends JdbcAccountPatchReviewStore {
 
   @VisibleForTesting
   public static class InMemoryModule extends LifecycleModule {
@@ -65,15 +40,9 @@
     }
   }
 
-  private final DataSource ds;
-
   @Inject
   H2AccountPatchReviewStore(@GerritServerConfig Config cfg, SitePaths sitePaths) {
-    this.ds = createDataSource(H2.appendUrlOptions(cfg, getUrl(sitePaths)));
-  }
-
-  public static String getUrl(SitePaths sitePaths) {
-    return H2.createUrl(sitePaths.db_dir.resolve("account_patch_reviews"));
+    super(cfg, sitePaths);
   }
 
   /**
@@ -85,196 +54,11 @@
     // DB_CLOSE_DELAY=-1: By default the content of an in-memory H2 database is
     // lost at the moment the last connection is closed. This option keeps the
     // content as long as the vm lives.
-    this.ds = createDataSource("jdbc:h2:mem:account_patch_reviews;DB_CLOSE_DELAY=-1");
-  }
-
-  private static DataSource createDataSource(String url) {
-    BasicDataSource datasource = new BasicDataSource();
-    datasource.setDriverClassName("org.h2.Driver");
-    datasource.setUrl(url);
-    datasource.setMaxActive(50);
-    datasource.setMinIdle(4);
-    datasource.setMaxIdle(16);
-    long evictIdleTimeMs = 1000 * 60;
-    datasource.setMinEvictableIdleTimeMillis(evictIdleTimeMs);
-    datasource.setTimeBetweenEvictionRunsMillis(evictIdleTimeMs / 2);
-    return datasource;
+    super(createDataSource("jdbc:h2:mem:account_patch_reviews;DB_CLOSE_DELAY=-1"));
   }
 
   @Override
-  public void start() {
-    try {
-      createTableIfNotExists();
-    } catch (OrmException e) {
-      log.error("Failed to create table to store account patch reviews", e);
-    }
-  }
-
-  public static void createTableIfNotExists(String url) throws OrmException {
-    try (Connection con = DriverManager.getConnection(url);
-        Statement stmt = con.createStatement()) {
-      doCreateTable(stmt);
-    } catch (SQLException e) {
-      throw convertError("create", e);
-    }
-  }
-
-  private void createTableIfNotExists() throws OrmException {
-    try (Connection con = ds.getConnection();
-        Statement stmt = con.createStatement()) {
-      doCreateTable(stmt);
-    } catch (SQLException e) {
-      throw convertError("create", e);
-    }
-  }
-
-  private static void doCreateTable(Statement stmt) throws SQLException {
-    stmt.executeUpdate(
-        "CREATE TABLE IF NOT EXISTS account_patch_reviews ("
-            + "account_id INTEGER DEFAULT 0 NOT NULL, "
-            + "change_id INTEGER DEFAULT 0 NOT NULL, "
-            + "patch_set_id INTEGER DEFAULT 0 NOT NULL, "
-            + "file_name VARCHAR(4096) DEFAULT '' NOT NULL, "
-            + "CONSTRAINT primary_key_account_patch_reviews "
-            + "PRIMARY KEY (account_id, change_id, patch_set_id, file_name)"
-            + ")");
-  }
-
-  public static void dropTableIfExists(String url) throws OrmException {
-    try (Connection con = DriverManager.getConnection(url);
-        Statement stmt = con.createStatement()) {
-      stmt.executeUpdate("DROP TABLE IF EXISTS account_patch_reviews");
-    } catch (SQLException e) {
-      throw convertError("create", e);
-    }
-  }
-
-  @Override
-  public void stop() {}
-
-  @Override
-  public boolean markReviewed(PatchSet.Id psId, Account.Id accountId, String path)
-      throws OrmException {
-    try (Connection con = ds.getConnection();
-        PreparedStatement stmt =
-            con.prepareStatement(
-                "INSERT INTO account_patch_reviews "
-                    + "(account_id, change_id, patch_set_id, file_name) VALUES "
-                    + "(?, ?, ?, ?)")) {
-      stmt.setInt(1, accountId.get());
-      stmt.setInt(2, psId.getParentKey().get());
-      stmt.setInt(3, psId.get());
-      stmt.setString(4, path);
-      stmt.executeUpdate();
-      return true;
-    } catch (SQLException e) {
-      OrmException ormException = convertError("insert", e);
-      if (ormException instanceof OrmDuplicateKeyException) {
-        return false;
-      }
-      throw ormException;
-    }
-  }
-
-  @Override
-  public void markReviewed(PatchSet.Id psId, Account.Id accountId, Collection<String> paths)
-      throws OrmException {
-    if (paths == null || paths.isEmpty()) {
-      return;
-    }
-
-    try (Connection con = ds.getConnection();
-        PreparedStatement stmt =
-            con.prepareStatement(
-                "INSERT INTO account_patch_reviews "
-                    + "(account_id, change_id, patch_set_id, file_name) VALUES "
-                    + "(?, ?, ?, ?)")) {
-      for (String path : paths) {
-        stmt.setInt(1, accountId.get());
-        stmt.setInt(2, psId.getParentKey().get());
-        stmt.setInt(3, psId.get());
-        stmt.setString(4, path);
-        stmt.addBatch();
-      }
-      stmt.executeBatch();
-    } catch (SQLException e) {
-      OrmException ormException = convertError("insert", e);
-      if (ormException instanceof OrmDuplicateKeyException) {
-        return;
-      }
-      throw ormException;
-    }
-  }
-
-  @Override
-  public void clearReviewed(PatchSet.Id psId, Account.Id accountId, String path)
-      throws OrmException {
-    try (Connection con = ds.getConnection();
-        PreparedStatement stmt =
-            con.prepareStatement(
-                "DELETE FROM account_patch_reviews "
-                    + "WHERE account_id = ? AND change_id = ? AND "
-                    + "patch_set_id = ? AND file_name = ?")) {
-      stmt.setInt(1, accountId.get());
-      stmt.setInt(2, psId.getParentKey().get());
-      stmt.setInt(3, psId.get());
-      stmt.setString(4, path);
-      stmt.executeUpdate();
-    } catch (SQLException e) {
-      throw convertError("delete", e);
-    }
-  }
-
-  @Override
-  public void clearReviewed(PatchSet.Id psId) throws OrmException {
-    try (Connection con = ds.getConnection();
-        PreparedStatement stmt =
-            con.prepareStatement(
-                "DELETE FROM account_patch_reviews "
-                    + "WHERE change_id = ? AND patch_set_id = ?")) {
-      stmt.setInt(1, psId.getParentKey().get());
-      stmt.setInt(2, psId.get());
-      stmt.executeUpdate();
-    } catch (SQLException e) {
-      throw convertError("delete", e);
-    }
-  }
-
-  @Override
-  public Optional<PatchSetWithReviewedFiles> findReviewed(PatchSet.Id psId, Account.Id accountId)
-      throws OrmException {
-    try (Connection con = ds.getConnection();
-        PreparedStatement stmt =
-            con.prepareStatement(
-                "SELECT patch_set_id, file_name FROM account_patch_reviews APR1 "
-                    + "WHERE account_id = ? AND change_id = ? AND patch_set_id = "
-                    + "(SELECT MAX(patch_set_id) FROM account_patch_reviews APR2 WHERE "
-                    + "APR1.account_id = APR2.account_id "
-                    + "AND APR1.change_id = APR2.change_id "
-                    + "AND patch_set_id <= ?)")) {
-      stmt.setInt(1, accountId.get());
-      stmt.setInt(2, psId.getParentKey().get());
-      stmt.setInt(3, psId.get());
-      try (ResultSet rs = stmt.executeQuery()) {
-        if (rs.next()) {
-          PatchSet.Id id = new PatchSet.Id(psId.getParentKey(), rs.getInt("PATCH_SET_ID"));
-          ImmutableSet.Builder<String> builder = ImmutableSet.builder();
-          do {
-            builder.add(rs.getString("FILE_NAME"));
-          } while (rs.next());
-
-          return Optional.of(
-              AccountPatchReviewStore.PatchSetWithReviewedFiles.create(id, builder.build()));
-        }
-
-        return Optional.empty();
-      }
-    } catch (SQLException e) {
-      throw convertError("select", e);
-    }
-  }
-
-  public static OrmException convertError(String op, SQLException err) {
+  public OrmException convertError(String op, SQLException err) {
     switch (getSQLStateInt(err)) {
       case 23001: // UNIQUE CONSTRAINT VIOLATION
       case 23505: // DUPLICATE_KEY_1
@@ -287,23 +71,4 @@
         return new OrmException(op + " failure on account_patch_reviews", err);
     }
   }
-
-  private static String getSQLState(SQLException err) {
-    String ec;
-    SQLException next = err;
-    do {
-      ec = next.getSQLState();
-      next = next.getNextException();
-    } while (ec == null && next != null);
-    return ec;
-  }
-
-  private static int getSQLStateInt(SQLException err) {
-    String s = getSQLState(err);
-    if (s != null) {
-      Integer i = Ints.tryParse(s);
-      return i != null ? i : -1;
-    }
-    return 0;
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java
new file mode 100644
index 0000000..2e2875f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java
@@ -0,0 +1,320 @@
+// Copyright (C) 2017 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.common.collect.ImmutableSet;
+import com.google.common.primitives.Ints;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.change.AccountPatchReviewStore;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gwtorm.server.OrmDuplicateKeyException;
+import com.google.gwtorm.server.OrmException;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Collection;
+import java.util.Optional;
+import javax.sql.DataSource;
+import org.apache.commons.dbcp.BasicDataSource;
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public abstract class JdbcAccountPatchReviewStore
+    implements AccountPatchReviewStore, LifecycleListener {
+  private static final Logger log = LoggerFactory.getLogger(JdbcAccountPatchReviewStore.class);
+
+  public static class Module extends LifecycleModule {
+    private final Config cfg;
+
+    public Module(Config cfg) {
+      this.cfg = cfg;
+    }
+
+    @Override
+    protected void configure() {
+      String url = cfg.getString("accountPatchReviewDb", null, "url");
+      if (url == null || url.contains("h2")) {
+        DynamicItem.bind(binder(), AccountPatchReviewStore.class)
+            .to(H2AccountPatchReviewStore.class);
+        listener().to(H2AccountPatchReviewStore.class);
+      } else if (url.contains("postgresql")) {
+        DynamicItem.bind(binder(), AccountPatchReviewStore.class)
+            .to(PostgresqlAccountPatchReviewStore.class);
+        listener().to(PostgresqlAccountPatchReviewStore.class);
+      } else if (url.contains("mysql")) {
+        DynamicItem.bind(binder(), AccountPatchReviewStore.class)
+            .to(MysqlAccountPatchReviewStore.class);
+        listener().to(MysqlAccountPatchReviewStore.class);
+      } else {
+        throw new IllegalArgumentException(
+            "unsupported driver type for account patch reviews db: " + url);
+      }
+    }
+  }
+
+  private final DataSource ds;
+
+  public static JdbcAccountPatchReviewStore createAccountPatchReviewStore(
+      Config cfg, SitePaths sitePaths) {
+    String url = cfg.getString("accountPatchReviewDb", null, "url");
+    if (url == null || url.contains("h2")) {
+      return new H2AccountPatchReviewStore(cfg, sitePaths);
+    } else if (url.contains("postgresql")) {
+      return new PostgresqlAccountPatchReviewStore(cfg, sitePaths);
+    } else if (url.contains("mysql")) {
+      return new MysqlAccountPatchReviewStore(cfg, sitePaths);
+    } else {
+      throw new IllegalArgumentException(
+          "unsupported driver type for account patch reviews db: " + url);
+    }
+  }
+
+  protected JdbcAccountPatchReviewStore(Config cfg, SitePaths sitePaths) {
+    this.ds = createDataSource(getUrl(cfg, sitePaths));
+  }
+
+  protected JdbcAccountPatchReviewStore(DataSource ds) {
+    this.ds = ds;
+  }
+
+  private static String getUrl(@GerritServerConfig Config cfg, SitePaths sitePaths) {
+    String url = cfg.getString("accountPatchReviewDb", null, "url");
+    if (url == null) {
+      return H2.createUrl(sitePaths.db_dir.resolve("account_patch_reviews"));
+    }
+    return url;
+  }
+
+  protected static DataSource createDataSource(String url) {
+    BasicDataSource datasource = new BasicDataSource();
+    if (url.contains("postgresql")) {
+      datasource.setDriverClassName("org.postgresql.Driver");
+    } else if (url.contains("h2")) {
+      datasource.setDriverClassName("org.h2.Driver");
+    } else if (url.contains("mysql")) {
+      datasource.setDriverClassName("com.mysql.jdbc.Driver");
+    }
+    datasource.setUrl(url);
+    datasource.setMaxActive(50);
+    datasource.setMinIdle(4);
+    datasource.setMaxIdle(16);
+    long evictIdleTimeMs = 1000 * 60;
+    datasource.setMinEvictableIdleTimeMillis(evictIdleTimeMs);
+    datasource.setTimeBetweenEvictionRunsMillis(evictIdleTimeMs / 2);
+    return datasource;
+  }
+
+  @Override
+  public void start() {
+    try {
+      createTableIfNotExists();
+    } catch (OrmException e) {
+      log.error("Failed to create table to store account patch reviews", e);
+    }
+  }
+
+  public Connection getConnection() throws SQLException {
+    return ds.getConnection();
+  }
+
+  public void createTableIfNotExists() throws OrmException {
+    try (Connection con = ds.getConnection();
+        Statement stmt = con.createStatement()) {
+      doCreateTable(stmt);
+    } catch (SQLException e) {
+      throw convertError("create", e);
+    }
+  }
+
+  private static void doCreateTable(Statement stmt) throws SQLException {
+    stmt.executeUpdate(
+        "CREATE TABLE IF NOT EXISTS account_patch_reviews ("
+            + "account_id INTEGER DEFAULT 0 NOT NULL, "
+            + "change_id INTEGER DEFAULT 0 NOT NULL, "
+            + "patch_set_id INTEGER DEFAULT 0 NOT NULL, "
+            + "file_name VARCHAR(4096) DEFAULT '' NOT NULL, "
+            + "CONSTRAINT primary_key_account_patch_reviews "
+            + "PRIMARY KEY (account_id, change_id, patch_set_id, file_name)"
+            + ")");
+  }
+
+  public void dropTableIfExists() throws OrmException {
+    try (Connection con = ds.getConnection();
+        Statement stmt = con.createStatement()) {
+      stmt.executeUpdate("DROP TABLE IF EXISTS account_patch_reviews");
+    } catch (SQLException e) {
+      throw convertError("create", e);
+    }
+  }
+
+  @Override
+  public void stop() {}
+
+  @Override
+  public boolean markReviewed(PatchSet.Id psId, Account.Id accountId, String path)
+      throws OrmException {
+    try (Connection con = ds.getConnection();
+        PreparedStatement stmt =
+            con.prepareStatement(
+                "INSERT INTO account_patch_reviews "
+                    + "(account_id, change_id, patch_set_id, file_name) VALUES "
+                    + "(?, ?, ?, ?)")) {
+      stmt.setInt(1, accountId.get());
+      stmt.setInt(2, psId.getParentKey().get());
+      stmt.setInt(3, psId.get());
+      stmt.setString(4, path);
+      stmt.executeUpdate();
+      return true;
+    } catch (SQLException e) {
+      OrmException ormException = convertError("insert", e);
+      if (ormException instanceof OrmDuplicateKeyException) {
+        return false;
+      }
+      throw ormException;
+    }
+  }
+
+  @Override
+  public void markReviewed(PatchSet.Id psId, Account.Id accountId, Collection<String> paths)
+      throws OrmException {
+    if (paths == null || paths.isEmpty()) {
+      return;
+    }
+
+    try (Connection con = ds.getConnection();
+        PreparedStatement stmt =
+            con.prepareStatement(
+                "INSERT INTO account_patch_reviews "
+                    + "(account_id, change_id, patch_set_id, file_name) VALUES "
+                    + "(?, ?, ?, ?)")) {
+      for (String path : paths) {
+        stmt.setInt(1, accountId.get());
+        stmt.setInt(2, psId.getParentKey().get());
+        stmt.setInt(3, psId.get());
+        stmt.setString(4, path);
+        stmt.addBatch();
+      }
+      stmt.executeBatch();
+    } catch (SQLException e) {
+      OrmException ormException = convertError("insert", e);
+      if (ormException instanceof OrmDuplicateKeyException) {
+        return;
+      }
+      throw ormException;
+    }
+  }
+
+  @Override
+  public void clearReviewed(PatchSet.Id psId, Account.Id accountId, String path)
+      throws OrmException {
+    try (Connection con = ds.getConnection();
+        PreparedStatement stmt =
+            con.prepareStatement(
+                "DELETE FROM account_patch_reviews "
+                    + "WHERE account_id = ? AND change_id = ? AND "
+                    + "patch_set_id = ? AND file_name = ?")) {
+      stmt.setInt(1, accountId.get());
+      stmt.setInt(2, psId.getParentKey().get());
+      stmt.setInt(3, psId.get());
+      stmt.setString(4, path);
+      stmt.executeUpdate();
+    } catch (SQLException e) {
+      throw convertError("delete", e);
+    }
+  }
+
+  @Override
+  public void clearReviewed(PatchSet.Id psId) throws OrmException {
+    try (Connection con = ds.getConnection();
+        PreparedStatement stmt =
+            con.prepareStatement(
+                "DELETE FROM account_patch_reviews "
+                    + "WHERE change_id = ? AND patch_set_id = ?")) {
+      stmt.setInt(1, psId.getParentKey().get());
+      stmt.setInt(2, psId.get());
+      stmt.executeUpdate();
+    } catch (SQLException e) {
+      throw convertError("delete", e);
+    }
+  }
+
+  @Override
+  public Optional<PatchSetWithReviewedFiles> findReviewed(PatchSet.Id psId, Account.Id accountId)
+      throws OrmException {
+    try (Connection con = ds.getConnection();
+        PreparedStatement stmt =
+            con.prepareStatement(
+                "SELECT patch_set_id, file_name FROM account_patch_reviews APR1 "
+                    + "WHERE account_id = ? AND change_id = ? AND patch_set_id = "
+                    + "(SELECT MAX(patch_set_id) FROM account_patch_reviews APR2 WHERE "
+                    + "APR1.account_id = APR2.account_id "
+                    + "AND APR1.change_id = APR2.change_id "
+                    + "AND patch_set_id <= ?)")) {
+      stmt.setInt(1, accountId.get());
+      stmt.setInt(2, psId.getParentKey().get());
+      stmt.setInt(3, psId.get());
+      try (ResultSet rs = stmt.executeQuery()) {
+        if (rs.next()) {
+          PatchSet.Id id = new PatchSet.Id(psId.getParentKey(), rs.getInt("patch_set_id"));
+          ImmutableSet.Builder<String> builder = ImmutableSet.builder();
+          do {
+            builder.add(rs.getString("file_name"));
+          } while (rs.next());
+
+          return Optional.of(
+              AccountPatchReviewStore.PatchSetWithReviewedFiles.create(id, builder.build()));
+        }
+
+        return Optional.empty();
+      }
+    } catch (SQLException e) {
+      throw convertError("select", e);
+    }
+  }
+
+  public OrmException convertError(String op, SQLException err) {
+    if (err.getCause() == null && err.getNextException() != null) {
+      err.initCause(err.getNextException());
+    }
+    return new OrmException(op + " failure on account_patch_reviews", err);
+  }
+
+  private static String getSQLState(SQLException err) {
+    String ec;
+    SQLException next = err;
+    do {
+      ec = next.getSQLState();
+      next = next.getNextException();
+    } while (ec == null && next != null);
+    return ec;
+  }
+
+  protected static int getSQLStateInt(SQLException err) {
+    String s = getSQLState(err);
+    if (s != null) {
+      Integer i = Ints.tryParse(s);
+      return i != null ? i : -1;
+    }
+    return 0;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/MysqlAccountPatchReviewStore.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/MysqlAccountPatchReviewStore.java
new file mode 100644
index 0000000..12ea6b6
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/MysqlAccountPatchReviewStore.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2017 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.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gwtorm.server.OrmDuplicateKeyException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.sql.SQLException;
+import org.eclipse.jgit.lib.Config;
+
+@Singleton
+public class MysqlAccountPatchReviewStore extends JdbcAccountPatchReviewStore {
+
+  @Inject
+  MysqlAccountPatchReviewStore(@GerritServerConfig Config cfg, SitePaths sitePaths) {
+    super(cfg, sitePaths);
+  }
+
+  @Override
+  public OrmException convertError(String op, SQLException err) {
+    switch (getSQLStateInt(err)) {
+      case 1022: // ER_DUP_KEY
+      case 1062: // ER_DUP_ENTRY
+      case 1169: // ER_DUP_UNIQUE;
+        return new OrmDuplicateKeyException("ACCOUNT_PATCH_REVIEWS", err);
+
+      default:
+        if (err.getCause() == null && err.getNextException() != null) {
+          err.initCause(err.getNextException());
+        }
+        return new OrmException(op + " failure on ACCOUNT_PATCH_REVIEWS", err);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/PostgresqlAccountPatchReviewStore.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/PostgresqlAccountPatchReviewStore.java
new file mode 100644
index 0000000..d228b91
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/PostgresqlAccountPatchReviewStore.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2017 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.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gwtorm.server.OrmDuplicateKeyException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.sql.SQLException;
+import org.eclipse.jgit.lib.Config;
+
+@Singleton
+public class PostgresqlAccountPatchReviewStore extends JdbcAccountPatchReviewStore {
+
+  @Inject
+  PostgresqlAccountPatchReviewStore(@GerritServerConfig Config cfg, SitePaths sitePaths) {
+    super(cfg, sitePaths);
+  }
+
+  @Override
+  public OrmException convertError(String op, SQLException err) {
+    switch (getSQLStateInt(err)) {
+      case 23505: // DUPLICATE_KEY_1
+        return new OrmDuplicateKeyException("ACCOUNT_PATCH_REVIEWS", err);
+
+      case 23514: // CHECK CONSTRAINT VIOLATION
+      case 23503: // FOREIGN KEY CONSTRAINT VIOLATION
+      case 23502: // NOT NULL CONSTRAINT VIOLATION
+      case 23001: // RESTRICT VIOLATION
+      default:
+        if (err.getCause() == null && err.getNextException() != null) {
+          err.initCause(err.getNextException());
+        }
+        return new OrmException(op + " failure on ACCOUNT_PATCH_REVIEWS", err);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java
index 3e9e4da..084d63b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.group.SystemGroupBackend;
@@ -39,6 +40,7 @@
 import java.sql.SQLException;
 import java.util.Collections;
 import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.PersonIdent;
 
 /** Creates or updates the current database schema. */
@@ -75,6 +77,7 @@
                 new Key<?>[] {
                   Key.get(PersonIdent.class, GerritPersonIdent.class),
                   Key.get(String.class, AnonymousCowardName.class),
+                  Key.get(Config.class, GerritServerConfig.class),
                 }) {
               rebind(parent, k);
             }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_127.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_127.java
index 75cdef7..aa28583 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_127.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_127.java
@@ -15,34 +15,38 @@
 package com.google.gerrit.server.schema;
 
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.sql.Connection;
-import java.sql.DriverManager;
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.sql.Statement;
+import org.eclipse.jgit.lib.Config;
 
 public class Schema_127 extends SchemaVersion {
   private static final int MAX_BATCH_SIZE = 1000;
 
   private final SitePaths sitePaths;
+  private final Config cfg;
 
   @Inject
-  Schema_127(Provider<Schema_126> prior, SitePaths sitePaths) {
+  Schema_127(Provider<Schema_126> prior, SitePaths sitePaths, @GerritServerConfig Config cfg) {
     super(prior);
     this.sitePaths = sitePaths;
+    this.cfg = cfg;
   }
 
   @Override
   protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
-    String url = H2AccountPatchReviewStore.getUrl(sitePaths);
-    H2AccountPatchReviewStore.dropTableIfExists(url);
-    H2AccountPatchReviewStore.createTableIfNotExists(url);
-    try (Connection con = DriverManager.getConnection(url);
+    JdbcAccountPatchReviewStore jdbcAccountPatchReviewStore =
+        JdbcAccountPatchReviewStore.createAccountPatchReviewStore(cfg, sitePaths);
+    jdbcAccountPatchReviewStore.dropTableIfExists();
+    jdbcAccountPatchReviewStore.createTableIfNotExists();
+    try (Connection con = jdbcAccountPatchReviewStore.getConnection();
         PreparedStatement stmt =
             con.prepareStatement(
                 "INSERT INTO account_patch_reviews "
@@ -69,7 +73,7 @@
         stmt.executeBatch();
       }
     } catch (SQLException e) {
-      throw H2AccountPatchReviewStore.convertError("insert", e);
+      throw jdbcAccountPatchReviewStore.convertError("insert", e);
     }
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexActivateCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexActivateCommand.java
index bff14dd..6b3e6d7 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexActivateCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexActivateCommand.java
@@ -17,7 +17,7 @@
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.lucene.LuceneVersionManager;
-import com.google.gerrit.lucene.ReindexerAlreadyRunningException;
+import com.google.gerrit.server.index.ReindexerAlreadyRunningException;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexStartCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexStartCommand.java
index cee016c..fb9b482 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexStartCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexStartCommand.java
@@ -17,7 +17,7 @@
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.lucene.LuceneVersionManager;
-import com.google.gerrit.lucene.ReindexerAlreadyRunningException;
+import com.google.gerrit.server.index.ReindexerAlreadyRunningException;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
index 59f1c75..a4e8e3c 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
@@ -66,7 +66,7 @@
 import com.google.gerrit.server.schema.DataSourceProvider;
 import com.google.gerrit.server.schema.DataSourceType;
 import com.google.gerrit.server.schema.DatabaseModule;
-import com.google.gerrit.server.schema.H2AccountPatchReviewStore;
+import com.google.gerrit.server.schema.JdbcAccountPatchReviewStore;
 import com.google.gerrit.server.schema.SchemaModule;
 import com.google.gerrit.server.schema.SchemaVersionCheck;
 import com.google.gerrit.server.securestore.SecureStoreClassName;
@@ -308,7 +308,7 @@
     modules.add(new DropWizardMetricMaker.RestModule());
     modules.add(new LogFileCompressor.Module());
     modules.add(new EventBroker.Module());
-    modules.add(new H2AccountPatchReviewStore.Module());
+    modules.add(new JdbcAccountPatchReviewStore.Module(config));
     modules.add(cfgInjector.getInstance(GitRepositoryManagerModule.class));
     modules.add(new StreamEventsApiListener.Module());
     modules.add(new ReceiveCommitsExecutorModule());