Schema_151: Attempt to add created_on column if it doesn't exist

If the created_on column does not exist, attempt to create it.

This allows the direct migration from 2.14 to 2.16 to succeed
without requiring an intermediate migration to 2.15.

Bug: Issue 10248
Change-Id: I626f1e26d43b60c3fd62ef3ef9ce3d7047c1a383
diff --git a/java/com/google/gerrit/server/schema/Schema_151.java b/java/com/google/gerrit/server/schema/Schema_151.java
index 41d8a32..bab9f41 100644
--- a/java/com/google/gerrit/server/schema/Schema_151.java
+++ b/java/com/google/gerrit/server/schema/Schema_151.java
@@ -14,11 +14,15 @@
 
 package com.google.gerrit.server.schema;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.jdbc.JdbcSchema;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.sql.SQLException;
@@ -26,6 +30,7 @@
 import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Locale;
 import java.util.Optional;
 
 /** A schema which adds the 'created on' field to groups. */
@@ -37,6 +42,13 @@
 
   @Override
   protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
+    Connection connection = ((JdbcSchema) db).getConnection();
+    if (!createdOnColumnExists(connection)) {
+      try (Statement stmt = connection.createStatement()) {
+        stmt.execute("ALTER TABLE account_groups ADD COLUMN created_on TIMESTAMP NULL");
+      }
+    }
+
     try (PreparedStatement groupUpdate =
             prepareStatement(db, "UPDATE account_groups SET created_on = ? WHERE group_id = ?");
         PreparedStatement addedOnRetrieval =
@@ -56,6 +68,20 @@
     }
   }
 
+  @VisibleForTesting
+  public static boolean createdOnColumnExists(Connection connection) throws SQLException {
+    DatabaseMetaData metaData = connection.getMetaData();
+    boolean toUpper = metaData.storesUpperCaseIdentifiers();
+    return metaData
+        .getColumns(
+            null, null, convertCase(toUpper, "account_groups"), convertCase(toUpper, "created_on"))
+        .next();
+  }
+
+  private static String convertCase(boolean toUpper, String input) {
+    return toUpper ? input.toUpperCase(Locale.US) : input;
+  }
+
   private static Optional<Timestamp> getFirstTimeMentioned(
       PreparedStatement addedOnRetrieval, AccountGroup.Id groupId) throws SQLException {
     addedOnRetrieval.setInt(1, groupId.get());
diff --git a/javatests/com/google/gerrit/server/schema/Schema_150_to_151_Test.java b/javatests/com/google/gerrit/server/schema/Schema_150_to_151_Test.java
index 4d5db6d..2e268ee 100644
--- a/javatests/com/google/gerrit/server/schema/Schema_150_to_151_Test.java
+++ b/javatests/com/google/gerrit/server/schema/Schema_150_to_151_Test.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.server.schema.Schema_151.createdOnColumnExists;
 
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -151,6 +152,17 @@
     assertThat(createdOn).isEqualTo(AccountGroup.auditCreationInstantTs());
   }
 
+  @Test
+  public void createdOnIsAddedWhenItIsMissing() throws Exception {
+    assertThat(createdOnColumnExists(connection)).isTrue();
+    try (Statement deleteColumn = connection.createStatement()) {
+      deleteColumn.execute("ALTER TABLE account_groups DROP COLUMN created_on");
+    }
+    assertThat(createdOnColumnExists(connection)).isFalse();
+    schema151.migrateData(db, new TestUpdateUI());
+    assertThat(createdOnColumnExists(connection)).isTrue();
+  }
+
   private AccountGroup.Id createGroupInReviewDb(String name) throws Exception {
     AccountGroup group =
         new AccountGroup(