From: Aurelien Poscia Date: Mon, 27 Mar 2023 09:20:37 +0000 (+0200) Subject: SONAR-18728 Add DB migrations for SCM_ACCOUNTS X-Git-Tag: 10.1.0.73491~539 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=aacf684567dee8cbb717b1f69f1a12e4750324f6;p=sonarqube.git SONAR-18728 Add DB migrations for SCM_ACCOUNTS --- diff --git a/server/sonar-db-dao/src/schema/schema-sq.ddl b/server/sonar-db-dao/src/schema/schema-sq.ddl index f3b228c4440..371a507c20b 100644 --- a/server/sonar-db-dao/src/schema/schema-sq.ddl +++ b/server/sonar-db-dao/src/schema/schema-sq.ddl @@ -929,6 +929,12 @@ CREATE TABLE "SCIM_USERS"( ALTER TABLE "SCIM_USERS" ADD CONSTRAINT "PK_SCIM_USERS" PRIMARY KEY("SCIM_UUID"); CREATE UNIQUE INDEX "UNIQ_SCIM_USERS_USER_UUID" ON "SCIM_USERS"("USER_UUID" NULLS FIRST); +CREATE TABLE "SCM_ACCOUNTS"( + "USER_UUID" CHARACTER VARYING(255) NOT NULL, + "SCM_ACCOUNT" CHARACTER VARYING(100) NOT NULL +); +ALTER TABLE "SCM_ACCOUNTS" ADD CONSTRAINT "PK_SCM_ACCOUNTS" PRIMARY KEY("USER_UUID", "SCM_ACCOUNT"); + CREATE TABLE "SESSION_TOKENS"( "UUID" CHARACTER VARYING(40) NOT NULL, "USER_UUID" CHARACTER VARYING(255) NOT NULL, diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/DataChange.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/DataChange.java index 45b64b50f12..75235bbf7d0 100644 --- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/DataChange.java +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/DataChange.java @@ -88,6 +88,9 @@ public abstract class DataChange implements MigrationStep { public MassUpdate prepareMassUpdate() { return new MassUpdate(db, readConnection, writeConnection); } + public MassRowSplitter prepareMassRowSplitter() { + return new MassRowSplitter<>(db, readConnection, writeConnection); + } } } diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/MassRowSplitter.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/MassRowSplitter.java new file mode 100644 index 00000000000..c63e49f8b24 --- /dev/null +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/MassRowSplitter.java @@ -0,0 +1,106 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db.migration.step; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Function; +import org.sonar.core.util.ProgressLogger; +import org.sonar.db.Database; + +import static com.google.common.base.Preconditions.checkState; + +public class MassRowSplitter { + + private final Database db; + private final Connection readConnection; + private final Connection writeConnection; + private final AtomicLong counter = new AtomicLong(0L); + private final ProgressLogger progress = ProgressLogger.create(getClass(), counter); + private Select select; + private UpsertImpl insert; + private Function> rowSplitterFunction; + public MassRowSplitter(Database db, Connection readConnection, Connection writeConnection) { + this.db = db; + this.readConnection = readConnection; + this.writeConnection = writeConnection; + } + + public Select select(String sql) throws SQLException { + this.select = SelectImpl.create(db, readConnection, sql); + return this.select; + } + + public Upsert insert(String sql) throws SQLException { + this.insert = UpsertImpl.create(writeConnection, sql); + return this.insert; + } + + public void splitRow(Function> rowSplitterFunction) { + this.rowSplitterFunction = rowSplitterFunction; + } + + public void execute(SqlStatementPreparer sqlStatementPreparer) throws SQLException { + checkState(select != null && insert != null, "SELECT or UPDATE request not defined"); + checkState(rowSplitterFunction != null, "rowSplitterFunction not defined"); + progress.start(); + try { + select.scroll(row -> processSingleRow(sqlStatementPreparer, row, rowSplitterFunction)); + closeStatements(); + + progress.log(); + } finally { + progress.stop(); + } + } + + private void processSingleRow(SqlStatementPreparer sqlStatementPreparer, Select.Row row, + Function> rowNormalizer) throws SQLException { + + Set data = rowNormalizer.apply(row); + for (T datum : data) { + if (sqlStatementPreparer.handle(datum, insert)) { + insert.addBatch(); + } + } + counter.getAndIncrement(); + } + + private void closeStatements() throws SQLException { + if (insert.getBatchCount() > 0L) { + insert.execute().commit(); + } + insert.close(); + select.close(); + } + + @FunctionalInterface + public interface SqlStatementPreparer { + /** + * Convert some column values of a given row. + * + * @return true if the row must be updated, else false. If false, then the update parameter must not be touched. + */ + boolean handle(T insertData, Upsert update) throws SQLException; + } + +} diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v100/CreateScmAccountsTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v100/CreateScmAccountsTable.java new file mode 100644 index 00000000000..0cb9a6b8c50 --- /dev/null +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v100/CreateScmAccountsTable.java @@ -0,0 +1,50 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db.migration.version.v100; + +import com.google.common.annotations.VisibleForTesting; +import java.sql.SQLException; +import org.sonar.db.Database; +import org.sonar.server.platform.db.migration.sql.CreateTableBuilder; +import org.sonar.server.platform.db.migration.step.CreateTableChange; + +import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.USER_UUID_SIZE; +import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.newVarcharColumnDefBuilder; + +public class CreateScmAccountsTable extends CreateTableChange { + static final String TABLE_NAME = "scm_accounts"; + static final String USER_UUID_COLUMN_NAME = "user_uuid"; + static final String SCM_ACCOUNT_COLUMN_NAME = "scm_account"; + + @VisibleForTesting + static final int SCM_ACCOUNT_SIZE = 100; + + public CreateScmAccountsTable(Database db) { + super(db, TABLE_NAME); + } + + @Override + public void execute(Context context, String tableName) throws SQLException { + context.execute(new CreateTableBuilder(getDialect(), tableName) + .addPkColumn(newVarcharColumnDefBuilder().setColumnName(USER_UUID_COLUMN_NAME).setIsNullable(false).setLimit(USER_UUID_SIZE).build()) + .addPkColumn(newVarcharColumnDefBuilder().setColumnName(SCM_ACCOUNT_COLUMN_NAME).setIsNullable(false).setLimit(SCM_ACCOUNT_SIZE).build()) + .build()); + } +} diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v100/DbVersion100.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v100/DbVersion100.java index ba6d6a3169f..fb8b694cd97 100644 --- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v100/DbVersion100.java +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v100/DbVersion100.java @@ -58,6 +58,8 @@ public class DbVersion100 implements DbVersion { .add(10_0_014, "Drop any SCIM User provisioning, turning all users local", DropScimUserProvisioning.class) .add(10_0_015, "Add ncloc to 'Projects' table", AddNclocToProjects.class) .add(10_0_016, "Populate ncloc in 'Projects' table", PopulateNclocForForProjects.class) + .add(10_0_015, "Add 'scm_accounts' table", CreateScmAccountsTable.class) + .add(10_0_016, "Migrate scm accounts from 'users' to 'scm_accounts' table", MigrateScmAccountsFromUsersToScmAccounts.class) ; } } diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v100/MigrateScmAccountsFromUsersToScmAccounts.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v100/MigrateScmAccountsFromUsersToScmAccounts.java new file mode 100644 index 00000000000..11e780128c9 --- /dev/null +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v100/MigrateScmAccountsFromUsersToScmAccounts.java @@ -0,0 +1,80 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db.migration.version.v100; + +import com.google.common.annotations.VisibleForTesting; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.Set; +import org.apache.commons.lang.StringUtils; +import org.sonar.db.Database; +import org.sonar.server.platform.db.migration.step.DataChange; +import org.sonar.server.platform.db.migration.step.MassRowSplitter; +import org.sonar.server.platform.db.migration.step.Select; + +import static java.util.Collections.emptySet; +import static java.util.stream.Collectors.toSet; + +public class MigrateScmAccountsFromUsersToScmAccounts extends DataChange { + + @VisibleForTesting + static final char SCM_ACCOUNTS_SEPARATOR_CHAR = '\n'; + + public MigrateScmAccountsFromUsersToScmAccounts(Database db) { + super(db); + } + + @Override + protected void execute(Context context) throws SQLException { + MassRowSplitter massRowSplitter = context.prepareMassRowSplitter(); + + massRowSplitter.select("select u.uuid, u.scm_accounts from users u where u.active=? and not exists (select 1 from scm_accounts sa where sa.user_uuid = u.uuid)") + .setBoolean(1, true); + + massRowSplitter.insert("insert into scm_accounts (user_uuid, scm_account) values (?, ?)"); + + massRowSplitter.splitRow(MigrateScmAccountsFromUsersToScmAccounts::toScmAccountRows); + + massRowSplitter.execute((scmAccountRow, insert) -> { + insert.setString(1, scmAccountRow.userUuid()); + insert.setString(2, scmAccountRow.scmAccount()); + return true; + }); + } + + private static Set toScmAccountRows(Select.Row row) { + try { + String userUuid = row.getString(1); + String[] scmAccounts = StringUtils.split(row.getString(2), SCM_ACCOUNTS_SEPARATOR_CHAR); + if (scmAccounts == null) { + return emptySet(); + } + return Arrays.stream(scmAccounts) + .map(scmAccount -> new ScmAccountRow(userUuid, scmAccount)) + .collect(toSet()); + } catch (SQLException sqlException) { + throw new RuntimeException(sqlException); + } + } + + @VisibleForTesting + record ScmAccountRow(String userUuid, String scmAccount) { + } +} diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/step/DataChangeTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/step/DataChangeTest.java index 11f1a439264..e7fb4663463 100644 --- a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/step/DataChangeTest.java +++ b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/step/DataChangeTest.java @@ -23,10 +23,13 @@ import java.sql.SQLException; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import javax.annotation.Nullable; +import org.apache.commons.lang.StringUtils; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -34,6 +37,8 @@ import org.sonar.db.CoreDbTester; import org.sonar.server.platform.db.migration.step.Select.Row; import org.sonar.server.platform.db.migration.step.Select.RowReader; +import static java.util.Collections.emptySet; +import static java.util.stream.Collectors.toSet; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.Assert.fail; @@ -392,6 +397,60 @@ public class DataChangeTest { assertPerson(3L, "login3", 13L, true, "2014-01-25", 5.4d); } + @Test + public void row_splitter_should_split_correctly() throws Exception { + insertPersons(); + + new DataChange(db.database()) { + @Override + public void execute(Context context) throws SQLException { + MassRowSplitter massRowSplitter = context.prepareMassRowSplitter(); + massRowSplitter.select("select id, phone_numbers from persons where id>?").setLong(1, -2L); + massRowSplitter.splitRow(row -> { + try { + int personId = row.getInt(1); + String phoneNumbers = row.getString(2); + if (phoneNumbers == null) { + return emptySet(); + } + return Arrays.stream(StringUtils.split(phoneNumbers, '\n')) + .map(number -> new PhoneNumberRow(personId, number)) + .collect(toSet()); + } catch (SQLException e) { + throw new RuntimeException(e); + } + }); + + massRowSplitter.insert("insert into phone_numbers (person_id, phone_number) values (?, ?)"); + massRowSplitter.execute((row, insert) -> { + insert.setLong(1, row.personId()) + .setString(2, row.phoneNumber()); + return true; + }); + } + }.execute(); + + Set actualRows = getPhoneNumberRows(); + + assertThat(actualRows) + .containsExactlyInAnyOrder( + new PhoneNumberRow(1, "1"), + new PhoneNumberRow(1, "32234"), + new PhoneNumberRow(1, "42343"), + new PhoneNumberRow(2, "432423") + ); + } + + private Set getPhoneNumberRows() { + return db + .select("select person_id as personId, phone_number as phoneNumber from phone_numbers") + .stream() + .map(row -> new PhoneNumberRow((long) row.get("PERSONID"), (String) row.get("PHONENUMBER"))) + .collect(toSet()); + } + + private record PhoneNumberRow(long personId, String phoneNumber){} + @Test public void display_current_row_details_if_error_during_mass_update() throws Exception { insertPersons(); @@ -497,9 +556,9 @@ public class DataChangeTest { } private void insertPersons() throws ParseException { - insertPerson(1, "barbara", 56, false, "2014-01-25", 1.5d); - insertPerson(2, "emmerik", 14, true, "2014-01-25", 5.2d); - insertPerson(3, "morgan", 3, true, "2014-01-25", 5.4d); + insertPerson(1, "barbara", 56, false, "2014-01-25", 1.5d, "\n1\n32234\n42343\n"); + insertPerson(2, "emmerik", 14, true, "2014-01-25", 5.2d, "432423"); + insertPerson(3, "morgan", 3, true, "2014-01-25", 5.4d, null); } private void assertInitialPersons() throws ParseException { @@ -508,17 +567,19 @@ public class DataChangeTest { assertPerson(3L, "morgan", 3L, true, "2014-01-25", 5.4d); } - private void insertPerson(int id, String login, int age, boolean enabled, String updatedAt, double coeff) throws ParseException { + private void insertPerson(int id, String login, int age, boolean enabled, String updatedAt, double coeff, @Nullable String newLineSeparatedPhoneNumbers) throws ParseException { db.executeInsert("persons", "ID", id, "LOGIN", login, "AGE", age, "ENABLED", enabled, "UPDATED_AT", dateFormat.parse(updatedAt), - "COEFF", coeff); + "COEFF", coeff, + "PHONE_NUMBERS", newLineSeparatedPhoneNumbers); } - private void assertPerson(long id, @Nullable String login, @Nullable Long age, @Nullable Boolean enabled, @Nullable String updatedAt, @Nullable Double coeff) throws ParseException { + private void assertPerson(long id, @Nullable String login, @Nullable Long age, @Nullable Boolean enabled, @Nullable String updatedAt, @Nullable Double coeff) + throws ParseException { List> rows = db .select("select id as \"ID\", login as \"LOGIN\", age as \"AGE\", enabled as \"ENABLED\", coeff as \"COEFF\", updated_at as \"UPDATED\" from persons where id=" + id); assertThat(rows).describedAs("id=" + id).hasSize(1); diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v100/CreateScmAccountsTableTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v100/CreateScmAccountsTableTest.java new file mode 100644 index 00000000000..467606bbaaa --- /dev/null +++ b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v100/CreateScmAccountsTableTest.java @@ -0,0 +1,63 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db.migration.version.v100; + +import java.sql.SQLException; +import java.sql.Types; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.db.CoreDbTester; +import org.sonar.server.platform.db.migration.step.DdlChange; + +import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.USER_UUID_SIZE; +import static org.sonar.server.platform.db.migration.version.v100.CreateScmAccountsTable.SCM_ACCOUNT_COLUMN_NAME; +import static org.sonar.server.platform.db.migration.version.v100.CreateScmAccountsTable.SCM_ACCOUNT_SIZE; +import static org.sonar.server.platform.db.migration.version.v100.CreateScmAccountsTable.TABLE_NAME; +import static org.sonar.server.platform.db.migration.version.v100.CreateScmAccountsTable.USER_UUID_COLUMN_NAME; + +public class CreateScmAccountsTableTest { + @Rule + public final CoreDbTester db = CoreDbTester.createEmpty(); + + private final DdlChange createScmAccountsTable = new CreateScmAccountsTable(db.database()); + + @Test + public void migration_should_create_a_table() throws SQLException { + db.assertTableDoesNotExist(TABLE_NAME); + + createScmAccountsTable.execute(); + + db.assertTableExists(TABLE_NAME); + db.assertColumnDefinition(TABLE_NAME, USER_UUID_COLUMN_NAME, Types.VARCHAR, USER_UUID_SIZE, false); + db.assertColumnDefinition(TABLE_NAME, SCM_ACCOUNT_COLUMN_NAME, Types.VARCHAR, SCM_ACCOUNT_SIZE, false); + db.assertPrimaryKey(TABLE_NAME, "pk_scm_accounts", USER_UUID_COLUMN_NAME, SCM_ACCOUNT_COLUMN_NAME); + } + + @Test + public void migration_should_be_reentrant() throws SQLException { + db.assertTableDoesNotExist(TABLE_NAME); + + createScmAccountsTable.execute(); + // re-entrant + createScmAccountsTable.execute(); + + db.assertTableExists(TABLE_NAME); + } +} diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v100/MigrateScmAccountsFromUsersToScmAccountsTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v100/MigrateScmAccountsFromUsersToScmAccountsTest.java new file mode 100644 index 00000000000..afa1f8d793b --- /dev/null +++ b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v100/MigrateScmAccountsFromUsersToScmAccountsTest.java @@ -0,0 +1,164 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db.migration.version.v100; + +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import javax.annotation.Nullable; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.core.util.UuidFactory; +import org.sonar.core.util.UuidFactoryFast; +import org.sonar.db.CoreDbTester; +import org.sonar.server.platform.db.migration.step.DataChange; +import org.sonar.server.platform.db.migration.version.v100.MigrateScmAccountsFromUsersToScmAccounts.ScmAccountRow; + +import static java.lang.String.format; +import static java.util.stream.Collectors.toSet; +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.server.platform.db.migration.version.v100.MigrateScmAccountsFromUsersToScmAccounts.SCM_ACCOUNTS_SEPARATOR_CHAR; + +public class MigrateScmAccountsFromUsersToScmAccountsTest { + + private static final UuidFactory UUID_FACTORY = UuidFactoryFast.getInstance(); + private static final String SCM_ACCOUNT1 = "scmAccount"; + private static final String SCM_ACCOUNT2 = "scmAccount2"; + + @Rule + public final CoreDbTester db = CoreDbTester.createForSchema(MigrateScmAccountsFromUsersToScmAccountsTest.class, "schema.sql"); + + private final DataChange migrateScmAccountsFromUsersToScmAccounts = new MigrateScmAccountsFromUsersToScmAccounts(db.database()); + + @Test + public void execute_whenUserHasNullScmAccounts_doNotInsertInScmAccounts() throws SQLException { + insertUserAndGetUuid(null); + + migrateScmAccountsFromUsersToScmAccounts.execute(); + + Set scmAccounts = findAllScmAccounts(); + assertThat(scmAccounts).isEmpty(); + } + + @Test + public void execute_whenUserHasEmptyScmAccounts_doNotInsertInScmAccounts() throws SQLException { + insertUserAndGetUuid(""); + + migrateScmAccountsFromUsersToScmAccounts.execute(); + + Set scmAccounts = findAllScmAccounts(); + assertThat(scmAccounts).isEmpty(); + } + + @Test + public void execute_whenUserHasEmptyScmAccountsWithOneSeparator_doNotInsertInScmAccounts() throws SQLException { + insertUserAndGetUuid(String.valueOf(SCM_ACCOUNTS_SEPARATOR_CHAR)); + + migrateScmAccountsFromUsersToScmAccounts.execute(); + + Set scmAccounts = findAllScmAccounts(); + assertThat(scmAccounts).isEmpty(); + } + + @Test + public void execute_whenUserHasEmptyScmAccountsWithTwoSeparators_doNotInsertInScmAccounts() throws SQLException { + insertUserAndGetUuid(SCM_ACCOUNTS_SEPARATOR_CHAR + String.valueOf(SCM_ACCOUNTS_SEPARATOR_CHAR)); + + migrateScmAccountsFromUsersToScmAccounts.execute(); + + Set scmAccounts = findAllScmAccounts(); + assertThat(scmAccounts).isEmpty(); + } + + @Test + public void execute_whenUserHasOneScmAccountWithoutSeparator_insertsInScmAccounts() throws SQLException { + String userUuid = insertUserAndGetUuid(SCM_ACCOUNT1); + + migrateScmAccountsFromUsersToScmAccounts.execute(); + + Set scmAccounts = findAllScmAccounts(); + assertThat(scmAccounts).containsExactly(new ScmAccountRow(userUuid, SCM_ACCOUNT1)); + } + + @Test + public void execute_whenUserHasOneScmAccountWithSeparators_insertsInScmAccounts() throws SQLException { + String userUuid = insertUserAndGetUuid(format("%s%s%s", SCM_ACCOUNTS_SEPARATOR_CHAR, SCM_ACCOUNT1, SCM_ACCOUNTS_SEPARATOR_CHAR)); + + migrateScmAccountsFromUsersToScmAccounts.execute(); + + Set scmAccounts = findAllScmAccounts(); + assertThat(scmAccounts).containsExactly(new ScmAccountRow(userUuid, SCM_ACCOUNT1)); + } + + @Test + public void execute_whenUserHasTwoScmAccount_insertsInScmAccounts() throws SQLException { + String userUuid = insertUserAndGetUuid(format("%s%s%s%s%s", + SCM_ACCOUNTS_SEPARATOR_CHAR, SCM_ACCOUNT1, SCM_ACCOUNTS_SEPARATOR_CHAR, SCM_ACCOUNT2, SCM_ACCOUNTS_SEPARATOR_CHAR)); + + migrateScmAccountsFromUsersToScmAccounts.execute(); + + Set scmAccounts = findAllScmAccounts(); + assertThat(scmAccounts).containsExactlyInAnyOrder( + new ScmAccountRow(userUuid, SCM_ACCOUNT1), + new ScmAccountRow(userUuid, SCM_ACCOUNT2) + ); + } + + @Test + public void migration_should_be_reentrant() throws SQLException { + String userUuid = insertUserAndGetUuid(SCM_ACCOUNT1); + + migrateScmAccountsFromUsersToScmAccounts.execute(); + migrateScmAccountsFromUsersToScmAccounts.execute(); + + Set scmAccounts = findAllScmAccounts(); + assertThat(scmAccounts).containsExactly(new ScmAccountRow(userUuid, SCM_ACCOUNT1)); + } + + + private Set findAllScmAccounts() { + Set scmAccounts = db.select("select user_uuid USERUUID, scm_account SCMACCOUNT from scm_accounts") + .stream() + .map(row -> new ScmAccountRow((String) row.get("USERUUID"), (String) row.get("SCMACCOUNT"))) + .collect(toSet()); + return scmAccounts; + } + + private String insertUserAndGetUuid(@Nullable String scmAccounts) { + + Map map = new HashMap<>(); + String uuid = UUID_FACTORY.create(); + String login = "login_" + uuid; + map.put("UUID", uuid); + map.put("LOGIN", login); + map.put("HASH_METHOD", "tagada"); + map.put("EXTERNAL_LOGIN", login); + map.put("EXTERNAL_IDENTITY_PROVIDER", "sonarqube"); + map.put("EXTERNAL_ID", login); + map.put("CREATED_AT", System.currentTimeMillis()); + map.put("RESET_PASSWORD", false); + map.put("USER_LOCAL", true); + map.put("SCM_ACCOUNTS", scmAccounts); + db.executeInsert("users", map); + return uuid; + } + +} diff --git a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/step/DataChangeTest/schema.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/step/DataChangeTest/schema.sql index 7b38dd9dc58..7a0e17f4e45 100644 --- a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/step/DataChangeTest/schema.sql +++ b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/step/DataChangeTest/schema.sql @@ -4,5 +4,11 @@ CREATE TABLE "PERSONS" ( "AGE" INTEGER, "ENABLED" BOOLEAN, "UPDATED_AT" TIMESTAMP, - "COEFF" DOUBLE + "COEFF" DOUBLE, + "PHONE_NUMBERS" VARCHAR(500) +); + +CREATE TABLE "PHONE_NUMBERS" ( + "PERSON_ID" INTEGER NOT NULL, + "PHONE_NUMBER" VARCHAR(100) ); diff --git a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v100/CreateIndexesForScmAccountsTest/schema.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v100/CreateIndexesForScmAccountsTest/schema.sql new file mode 100644 index 00000000000..eca4564d0de --- /dev/null +++ b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v100/CreateIndexesForScmAccountsTest/schema.sql @@ -0,0 +1,5 @@ +CREATE TABLE "SCM_ACCOUNTS"( + "USER_UUID" CHARACTER VARYING(255) NOT NULL, + "SCM_ACCOUNT" CHARACTER VARYING(100) NOT NULL +); +ALTER TABLE "SCM_ACCOUNTS" ADD CONSTRAINT "PK_SCM_ACCOUNTS" PRIMARY KEY("USER_UUID", "SCM_ACCOUNT"); diff --git a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v100/MigrateScmAccountsFromUsersToScmAccountsTest/schema.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v100/MigrateScmAccountsFromUsersToScmAccountsTest/schema.sql new file mode 100644 index 00000000000..935cde8aa74 --- /dev/null +++ b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v100/MigrateScmAccountsFromUsersToScmAccountsTest/schema.sql @@ -0,0 +1,33 @@ +CREATE TABLE "USERS"( + "UUID" CHARACTER VARYING(255) NOT NULL, + "LOGIN" CHARACTER VARYING(255) NOT NULL, + "NAME" CHARACTER VARYING(200), + "EMAIL" CHARACTER VARYING(100), + "CRYPTED_PASSWORD" CHARACTER VARYING(100), + "SALT" CHARACTER VARYING(40), + "HASH_METHOD" CHARACTER VARYING(10), + "ACTIVE" BOOLEAN DEFAULT TRUE, + "SCM_ACCOUNTS" CHARACTER VARYING(4000), + "EXTERNAL_LOGIN" CHARACTER VARYING(255) NOT NULL, + "EXTERNAL_IDENTITY_PROVIDER" CHARACTER VARYING(100) NOT NULL, + "EXTERNAL_ID" CHARACTER VARYING(255) NOT NULL, + "USER_LOCAL" BOOLEAN NOT NULL, + "HOMEPAGE_TYPE" CHARACTER VARYING(40), + "HOMEPAGE_PARAMETER" CHARACTER VARYING(40), + "LAST_CONNECTION_DATE" BIGINT, + "CREATED_AT" BIGINT, + "UPDATED_AT" BIGINT, + "RESET_PASSWORD" BOOLEAN NOT NULL, + "LAST_SONARLINT_CONNECTION" BIGINT +); +ALTER TABLE "USERS" ADD CONSTRAINT "PK_USERS" PRIMARY KEY("UUID"); +CREATE UNIQUE INDEX "USERS_LOGIN" ON "USERS"("LOGIN" NULLS FIRST); +CREATE INDEX "USERS_UPDATED_AT" ON "USERS"("UPDATED_AT" NULLS FIRST); +CREATE UNIQUE INDEX "UNIQ_EXTERNAL_ID" ON "USERS"("EXTERNAL_IDENTITY_PROVIDER" NULLS FIRST, "EXTERNAL_ID" NULLS FIRST); +CREATE UNIQUE INDEX "UNIQ_EXTERNAL_LOGIN" ON "USERS"("EXTERNAL_IDENTITY_PROVIDER" NULLS FIRST, "EXTERNAL_LOGIN" NULLS FIRST); + +CREATE TABLE "SCM_ACCOUNTS"( + "USER_UUID" CHARACTER VARYING(255) NOT NULL, + "SCM_ACCOUNT" CHARACTER VARYING(100) NOT NULL +); +ALTER TABLE "SCM_ACCOUNTS" ADD CONSTRAINT "PK_SCM_ACCOUNTS" PRIMARY KEY("USER_UUID", "SCM_ACCOUNT");