From d7c1632113c8c1fe15766e3728a831a146c1de21 Mon Sep 17 00:00:00 2001 From: Claire Villard Date: Tue, 1 Oct 2024 18:42:45 +0200 Subject: [PATCH] SONAR-23213 'api/system/migrate_measures' endpoint --- .../org/sonar/db/component/BranchDao.java | 23 +- .../org/sonar/db/component/BranchMapper.java | 6 + .../org/sonar/db/portfolio/PortfolioDao.java | 23 +- .../sonar/db/portfolio/PortfolioMapper.java | 6 + .../org/sonar/db/component/BranchMapper.xml | 41 +++ .../sonar/db/portfolio/PortfolioMapper.xml | 46 +++- .../org/sonar/db/component/BranchDaoTest.java | 93 ++++--- .../sonar/db/portfolio/PortfolioDaoTest.java | 82 ++++-- ...AbstractMigrateLiveMeasuresToMeasures.java | 196 +++++++++++++++ ...MigrateBranchesLiveMeasuresToMeasures.java | 30 +++ ...gratePortfoliosLiveMeasuresToMeasures.java | 30 +++ .../db/migration/step/DataChange.java | 4 +- ...ateBranchesLiveMeasuresToMeasuresTest.java | 180 ++++++++++++++ ...ePortfoliosLiveMeasuresToMeasuresTest.java | 178 +++++++++++++ .../schema.sql | 63 +++++ .../schema.sql | 64 +++++ .../platform/ws/MigrateMeasuresAction.java | 153 ++++++++++++ .../server/platform/ws/SystemWsModule.java | 6 + .../platform/ws/example-migrate_measures.json | 8 + .../ws/MigrateMeasuresActionTest.java | 233 ++++++++++++++++++ .../platform/ws/SystemWsModuleTest.java | 2 +- 21 files changed, 1411 insertions(+), 56 deletions(-) create mode 100644 server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/adhoc/AbstractMigrateLiveMeasuresToMeasures.java create mode 100644 server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/adhoc/MigrateBranchesLiveMeasuresToMeasures.java create mode 100644 server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/adhoc/MigratePortfoliosLiveMeasuresToMeasures.java create mode 100644 server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/adhoc/MigrateBranchesLiveMeasuresToMeasuresTest.java create mode 100644 server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/adhoc/MigratePortfoliosLiveMeasuresToMeasuresTest.java create mode 100644 server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/adhoc/MigrateBranchesLiveMeasuresToMeasuresTest/schema.sql create mode 100644 server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/adhoc/MigratePortfoliosLiveMeasuresToMeasuresTest/schema.sql create mode 100644 server/sonar-webserver-webapi/src/main/java/org/sonar/server/platform/ws/MigrateMeasuresAction.java create mode 100644 server/sonar-webserver-webapi/src/main/resources/org/sonar/server/platform/ws/example-migrate_measures.json create mode 100644 server/sonar-webserver-webapi/src/test/java/org/sonar/server/platform/ws/MigrateMeasuresActionTest.java diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchDao.java index 9bc71dc3534..402e09abe5f 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchDao.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchDao.java @@ -19,6 +19,7 @@ */ package org.sonar.db.component; +import com.google.common.annotations.VisibleForTesting; import java.util.Collection; import java.util.LinkedList; import java.util.List; @@ -136,6 +137,24 @@ public class BranchDao implements Dao { return mapper(session).countByNeedIssueSync(needIssueSync); } + public List selectUuidsWithMeasuresMigratedFalse(DbSession session, int limit) { + return mapper(session).selectUuidsWithMeasuresMigratedFalse(limit); + } + + public int countByMeasuresMigratedFalse(DbSession session) { + return mapper(session).countByMeasuresMigratedFalse(); + } + + public long updateMeasuresMigrated(DbSession dbSession, String branchUuid, boolean measuresMigrated) { + long now = system2.now(); + return mapper(dbSession).updateMeasuresMigrated(branchUuid, measuresMigrated, now); + } + + @VisibleForTesting + boolean isMeasuresMigrated(DbSession dbSession, String uuid) { + return mapper(dbSession).isMeasuresMigrated(uuid); + } + public int countAll(DbSession session) { return mapper(session).countAll(); } @@ -178,8 +197,4 @@ public class BranchDao implements Dao { return false; } - public long updateMeasuresMigrated(DbSession dbSession, String branchUuid, boolean measuresMigrated) { - long now = system2.now(); - return mapper(dbSession).updateMeasuresMigrated(branchUuid, measuresMigrated, now); - } } diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchMapper.java index 24cdbacc68c..95adbb62244 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchMapper.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchMapper.java @@ -73,4 +73,10 @@ public interface BranchMapper { short doAnyOfComponentsNeedIssueSync(@Param("componentKeys") List components); int updateMeasuresMigrated(@Param("uuid") String uuid, @Param("measuresMigrated") boolean measuresMigrated, @Param("now") long now); + + boolean isMeasuresMigrated(String uuid); + + List selectUuidsWithMeasuresMigratedFalse(int limit); + + int countByMeasuresMigratedFalse(); } diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/portfolio/PortfolioDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/portfolio/PortfolioDao.java index cbcfd25b3c6..0a6863b6fe6 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/portfolio/PortfolioDao.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/portfolio/PortfolioDao.java @@ -19,6 +19,7 @@ */ package org.sonar.db.portfolio; +import com.google.common.annotations.VisibleForTesting; import java.util.List; import java.util.Optional; import java.util.Set; @@ -253,6 +254,24 @@ public class PortfolioDao implements Dao { mapper(dbSession).deleteBranch(portfolioUuid, projectUuid, branchUuid); } + public long updateMeasuresMigrated(DbSession dbSession, String branchUuid, boolean measuresMigrated) { + long now = system2.now(); + return mapper(dbSession).updateMeasuresMigrated(branchUuid, measuresMigrated, now); + } + + public List selectUuidsWithMeasuresMigratedFalse(DbSession session, int limit) { + return mapper(session).selectUuidsWithMeasuresMigratedFalse(limit); + } + + public int countByMeasuresMigratedFalse(DbSession session) { + return mapper(session).countByMeasuresMigratedFalse(); + } + + @VisibleForTesting + boolean isMeasuresMigrated(DbSession dbSession, String uuid) { + return mapper(dbSession).isMeasuresMigrated(uuid); + } + /* * Utils */ @@ -268,8 +287,4 @@ public class PortfolioDao implements Dao { return portfolioDto.isRoot() ? Qualifiers.VIEW : Qualifiers.SUBVIEW; } - public long updateMeasuresMigrated(DbSession dbSession, String branchUuid, boolean measuresMigrated) { - long now = system2.now(); - return mapper(dbSession).updateMeasuresMigrated(branchUuid, measuresMigrated, now); - } } diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/portfolio/PortfolioMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/portfolio/PortfolioMapper.java index 2e273acc6d7..1dbb66749bb 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/portfolio/PortfolioMapper.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/portfolio/PortfolioMapper.java @@ -113,4 +113,10 @@ public interface PortfolioMapper { List selectRootOfReferencersToAppBranch(@Param("appUuid") String appUuid, @Param("appBranchKey") String appBranchKey); int updateMeasuresMigrated(@Param("uuid") String uuid, @Param("measuresMigrated") boolean measuresMigrated, @Param("now") long now); + + List selectUuidsWithMeasuresMigratedFalse(int limit); + + int countByMeasuresMigratedFalse(); + + boolean isMeasuresMigrated(@Param("uuid") String branchUuid); } diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/component/BranchMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/component/BranchMapper.xml index eafb0b008cb..238da88d1bd 100644 --- a/server/sonar-db-dao/src/main/resources/org/sonar/db/component/BranchMapper.xml +++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/component/BranchMapper.xml @@ -256,4 +256,45 @@ where uuid = #{uuid, jdbcType=VARCHAR} + + + + + + + + + + + diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/portfolio/PortfolioMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/portfolio/PortfolioMapper.xml index 832fc96bb4a..db973bc0b50 100644 --- a/server/sonar-db-dao/src/main/resources/org/sonar/db/portfolio/PortfolioMapper.xml +++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/portfolio/PortfolioMapper.xml @@ -544,6 +544,50 @@ measures_migrated = #{measuresMigrated, jdbcType=BOOLEAN}, updated_at = #{now, jdbcType=BIGINT} where - uuid = #{uuid, jdbcType=VARCHAR} + root_uuid = #{uuid, jdbcType=VARCHAR} + + + + + + + + + + diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/component/BranchDaoTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/component/BranchDaoTest.java index 8f42fc67056..f11f90f271c 100644 --- a/server/sonar-db-dao/src/test/java/org/sonar/db/component/BranchDaoTest.java +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/component/BranchDaoTest.java @@ -39,13 +39,11 @@ import org.sonar.api.impl.utils.TestSystem2; import org.sonar.api.utils.System2; import org.sonar.db.DbSession; import org.sonar.db.DbTester; -import org.sonar.db.dialect.Oracle; import org.sonar.db.metric.MetricDto; import org.sonar.db.project.ProjectDto; import org.sonar.db.protobuf.DbProjectBranches; import org.sonar.server.platform.db.migration.adhoc.AddMeasuresMigratedColumnToProjectBranchesTable; -import static java.lang.String.format; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; @@ -655,6 +653,70 @@ public class BranchDaoTest { assertThat(underTest.countByNeedIssueSync(dbSession, false)).isEqualTo(4); } + @Test + public void selectUuidsWithMeasuresMigratedFalse() throws SQLException { + createMeasuresMigratedColumn(); + + // master branch with flag set to false + ComponentDto notMigratedProject = db.components().insertPrivateProject(); + // branches & PRs + ComponentDto migratedBranch = db.components().insertProjectBranch(notMigratedProject, b -> b.setBranchType(BRANCH)); + ComponentDto notMigratedBranch = db.components().insertProjectBranch(notMigratedProject, b -> b.setBranchType(BranchType.BRANCH)); + ComponentDto migratedPR = db.components().insertProjectBranch(notMigratedProject, b -> b.setBranchType(BranchType.PULL_REQUEST)); + ComponentDto notMigratedPR = db.components().insertProjectBranch(notMigratedProject, b -> b.setBranchType(PULL_REQUEST)); + + db.getDbClient().branchDao().updateMeasuresMigrated(dbSession, migratedBranch.branchUuid(), true); + db.getDbClient().branchDao().updateMeasuresMigrated(dbSession, notMigratedBranch.branchUuid(), false); + db.getDbClient().branchDao().updateMeasuresMigrated(dbSession, migratedPR.branchUuid(), true); + + assertThat(underTest.selectUuidsWithMeasuresMigratedFalse(dbSession, 10)) + .hasSize(3) + .containsOnly(notMigratedProject.branchUuid(), notMigratedBranch.branchUuid(), notMigratedPR.branchUuid()); + + assertThat(underTest.selectUuidsWithMeasuresMigratedFalse(dbSession, 1)) + .hasSize(1) + .containsOnly(notMigratedProject.branchUuid()); + } + + @Test + public void countByMeasuresMigratedFalse() throws SQLException { + createMeasuresMigratedColumn(); + + // master branch with flag set to false + ComponentDto project = db.components().insertPrivateProject(); + // branches & PRs + ComponentDto branch1 = db.components().insertProjectBranch(project, b -> b.setBranchType(BRANCH)); + ComponentDto branch2 = db.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.BRANCH)); + ComponentDto branch3 = db.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.PULL_REQUEST)); + db.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.PULL_REQUEST)); + + db.getDbClient().branchDao().updateMeasuresMigrated(dbSession, branch1.branchUuid(), true); + db.getDbClient().branchDao().updateMeasuresMigrated(dbSession, branch2.branchUuid(), false); + db.getDbClient().branchDao().updateMeasuresMigrated(dbSession, branch3.branchUuid(), true); + + assertThat(underTest.countByMeasuresMigratedFalse(dbSession)).isEqualTo(3); + } + + @Test + public void updateMeasuresMigrated() throws SQLException { + createMeasuresMigratedColumn(); + + ComponentDto project = db.components().insertPrivateProject(); + String uuid1 = db.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.BRANCH)).uuid(); + String uuid2 = db.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.BRANCH)).uuid(); + + underTest.updateMeasuresMigrated(dbSession, uuid1, true); + underTest.updateMeasuresMigrated(dbSession, uuid2, false); + + assertThat(underTest.isMeasuresMigrated(dbSession, uuid1)).isTrue(); + assertThat(underTest.isMeasuresMigrated(dbSession, uuid2)).isFalse(); + } + + private void createMeasuresMigratedColumn() throws SQLException { + AddMeasuresMigratedColumnToProjectBranchesTable migration = new AddMeasuresMigratedColumnToProjectBranchesTable(db.getDbClient().getDatabase()); + migration.execute(); + } + @Test public void countAll() { assertThat(underTest.countAll(dbSession)).isZero(); @@ -780,31 +842,4 @@ public class BranchDaoTest { assertThat(underTest.doAnyOfComponentsNeedIssueSync(dbSession, componentKeys)).isTrue(); } - - @Test - public void updateMeasuresMigrated() throws SQLException { - new AddMeasuresMigratedColumnToProjectBranchesTable(db.getDbClient().getDatabase()).execute(); - - ComponentDto project = db.components().insertPrivateProject(); - String uuid1 = db.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.BRANCH)).uuid(); - String uuid2 = db.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.BRANCH)).uuid(); - - underTest.updateMeasuresMigrated(dbSession, uuid1, true); - underTest.updateMeasuresMigrated(dbSession, uuid2, false); - - assertThat(getMeasuresMigrated(uuid1)).isTrue(); - assertThat(getMeasuresMigrated(uuid2)).isFalse(); - } - - private boolean getMeasuresMigrated(String uuid1) { - List> select = db.select(dbSession, - format("select measures_migrated as \"MIGRATED\" from project_branches where uuid = '%s'", uuid1)); - - assertThat(select).hasSize(1); - Object value = select.get(0).get("MIGRATED"); - if (db.getDbClient().getDatabase().getDialect().getId().equals(Oracle.ID)) { - return (long) value == 1; - } - return (boolean) value; - } } diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/portfolio/PortfolioDaoTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/portfolio/PortfolioDaoTest.java index ae12b1b059d..9ea18c73c0e 100644 --- a/server/sonar-db-dao/src/test/java/org/sonar/db/portfolio/PortfolioDaoTest.java +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/portfolio/PortfolioDaoTest.java @@ -20,8 +20,6 @@ package org.sonar.db.portfolio; import java.sql.SQLException; -import java.util.List; -import java.util.Map; import java.util.Set; import org.junit.Rule; import org.junit.Test; @@ -32,12 +30,10 @@ import org.sonar.db.DbSession; import org.sonar.db.DbTester; import org.sonar.db.audit.AuditPersister; import org.sonar.db.component.BranchDto; -import org.sonar.db.dialect.Oracle; import org.sonar.db.project.ApplicationProjectDto; import org.sonar.db.project.ProjectDto; import org.sonar.server.platform.db.migration.adhoc.AddMeasuresMigratedColumnToPortfoliosTable; -import static java.lang.String.format; import static java.util.Collections.emptySet; import static java.util.Collections.singleton; import static org.assertj.core.api.Assertions.assertThat; @@ -715,20 +711,76 @@ public class PortfolioDaoTest { portfolioDao.updateMeasuresMigrated(session, portfolio1.getUuid(), true); portfolioDao.updateMeasuresMigrated(session, portfolio2.getUuid(), false); - assertThat(getMeasuresMigrated(portfolio1.getUuid())).isTrue(); - assertThat(getMeasuresMigrated(portfolio2.getUuid())).isFalse(); + assertThat(portfolioDao.isMeasuresMigrated(session, portfolio1.getUuid())).isTrue(); + assertThat(portfolioDao.isMeasuresMigrated(session, portfolio2.getUuid())).isFalse(); } - private boolean getMeasuresMigrated(String uuid1) { - List> select = db.select(session, - format("select measures_migrated as \"MIGRATED\" from portfolios where uuid = '%s'", uuid1)); + @Test + public void selectUuidsWithMeasuresMigratedFalse() throws SQLException { + createMeasuresMigratedColumn(); + + PortfolioDto migratedRoot = db.components().insertPrivatePortfolioDto(); + PortfolioDto migratedChild1 = addPortfolio(migratedRoot); + PortfolioDto notMigratedRoot = db.components().insertPrivatePortfolioDto(); + PortfolioDto notMigratedChild = addPortfolio(notMigratedRoot); + PortfolioDto notMigratedRoot2 = db.components().insertPrivatePortfolioDto(); + + // a parent portfolio and its children are expected to have the same migration status + db.getDbClient().portfolioDao().updateMeasuresMigrated(session, migratedRoot.getUuid(), true); + db.getDbClient().portfolioDao().updateMeasuresMigrated(session, migratedChild1.getUuid(), true); + db.getDbClient().portfolioDao().updateMeasuresMigrated(session, notMigratedRoot.getUuid(), false); + db.getDbClient().portfolioDao().updateMeasuresMigrated(session, notMigratedChild.getUuid(), false); + + assertThat(portfolioDao.selectUuidsWithMeasuresMigratedFalse(session, 10)) + .hasSize(2) + .containsOnly(notMigratedRoot.getUuid(), notMigratedRoot2.getUuid()); + + assertThat(portfolioDao.selectUuidsWithMeasuresMigratedFalse(session, 1)) + .hasSize(1) + .containsOnly(notMigratedRoot.getUuid()); + } + + @Test + public void countByMeasuresMigratedFalse() throws SQLException { + createMeasuresMigratedColumn(); + + PortfolioDto root = db.components().insertPrivatePortfolioDto(); + PortfolioDto child1 = addPortfolio(root); + PortfolioDto root2 = db.components().insertPrivatePortfolioDto(); + PortfolioDto child2 = addPortfolio(root2); + PortfolioDto root3 = db.components().insertPrivatePortfolioDto(); + db.components().insertPrivatePortfolioDto(); + + // a parent portfolio and its children are expected to have the same migration status + db.getDbClient().portfolioDao().updateMeasuresMigrated(session, root.getUuid(), true); + db.getDbClient().portfolioDao().updateMeasuresMigrated(session, child1.getUuid(), true); + db.getDbClient().portfolioDao().updateMeasuresMigrated(session, root2.getUuid(), false); + db.getDbClient().portfolioDao().updateMeasuresMigrated(session, child2.getUuid(), false); + db.getDbClient().portfolioDao().updateMeasuresMigrated(session, root3.getUuid(), false); + + assertThat(portfolioDao.countByMeasuresMigratedFalse(session)).isEqualTo(3); + } + + @Test + public void updateMeasuresMigrated() throws SQLException { + createMeasuresMigratedColumn(); + + PortfolioDto portfolio1 = db.components().insertPrivatePortfolioDto("name1"); + PortfolioDto portfolio2 = db.components().insertPrivatePortfolioDto("name2"); + PortfolioDto portfolio3 = db.components().insertPrivatePortfolioDto("name3", + p -> p.setRootUuid(portfolio1.getUuid()).setParentUuid(portfolio1.getUuid())); + + portfolioDao.updateMeasuresMigrated(session, portfolio1.getUuid(), true); + portfolioDao.updateMeasuresMigrated(session, portfolio2.getUuid(), false); + + assertThat(portfolioDao.isMeasuresMigrated(session, portfolio1.getUuid())).isTrue(); + assertThat(portfolioDao.isMeasuresMigrated(session, portfolio2.getUuid())).isFalse(); + assertThat(portfolioDao.isMeasuresMigrated(session, portfolio3.getUuid())).isTrue(); + } - assertThat(select).hasSize(1); - Object value = select.get(0).get("MIGRATED"); - if (db.getDbClient().getDatabase().getDialect().getId().equals(Oracle.ID)) { - return (long) value == 1; - } - return (boolean) value; + private void createMeasuresMigratedColumn() throws SQLException { + AddMeasuresMigratedColumnToPortfoliosTable migration = new AddMeasuresMigratedColumnToPortfoliosTable(db.getDbClient().getDatabase()); + migration.execute(); } private PortfolioDto addPortfolio(PortfolioDto parent) { diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/adhoc/AbstractMigrateLiveMeasuresToMeasures.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/adhoc/AbstractMigrateLiveMeasuresToMeasures.java new file mode 100644 index 00000000000..5135d7aa859 --- /dev/null +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/adhoc/AbstractMigrateLiveMeasuresToMeasures.java @@ -0,0 +1,196 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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.adhoc; + +import com.google.gson.Gson; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import javax.annotation.Nullable; +import org.apache.commons.codec.digest.MurmurHash3; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.api.utils.System2; +import org.sonar.db.Database; +import org.sonar.server.platform.db.migration.step.DataChange; +import org.sonar.server.platform.db.migration.step.MassUpdate; +import org.sonar.server.platform.db.migration.step.Select; +import org.sonar.server.platform.db.migration.step.SqlStatement; +import org.sonar.server.platform.db.migration.step.Upsert; + +import static java.lang.String.format; +import static java.nio.charset.StandardCharsets.UTF_8; + +public abstract class AbstractMigrateLiveMeasuresToMeasures extends DataChange { + private static final Logger LOGGER = LoggerFactory.getLogger(AbstractMigrateLiveMeasuresToMeasures.class); + + private static final Set TEXT_VALUE_TYPES = Set.of("STRING", "LEVEL", "DATA", "DISTRIB"); + private static final Gson GSON = new Gson(); + + private static final String SELECT_QUERY = """ + SELECT lm.component_uuid, + m.name, + m.val_type, + lm.value, + lm.text_value, + lm.measure_data + FROM live_measures lm + INNER JOIN metrics m ON m.uuid = lm.metric_uuid + WHERE lm.project_uuid = ? + ORDER BY lm.component_uuid + """; + + private static final String INSERT_QUERY = """ + insert into measures (component_uuid, branch_uuid, json_value, json_value_hash, created_at, updated_at) + values ( ?, ?, ?, ?, ?, ?) + """; + + private final String tableName; + private final String item; + private final System2 system2; + private List uuids = List.of(); + private int migrated = 0; + + protected AbstractMigrateLiveMeasuresToMeasures(Database db, System2 system2, String tableName, String item) { + super(db); + this.system2 = system2; + this.tableName = tableName; + this.item = item; + } + + private String getUpdateFlagQuery() { + return format(""" + UPDATE %s + SET measures_migrated = ? + WHERE uuid = ? + """, tableName); + } + + // This is a special entry point for the case of a configurable migration + public final void migrate(List uuids) throws SQLException { + try (Connection readConnection = createDdlConnection(); + Connection writeConnection = createDdlConnection()) { + Context context = new Context(db, readConnection, writeConnection); + this.uuids = uuids; + execute(context); + } + } + + @Override + protected void execute(Context context) throws SQLException { + LOGGER.info("Starting the migration of {} {}s", uuids.size(), item); + + for (String uuid : uuids) { + migrateItem(uuid, context); + + migrated++; + if (migrated % 100 == 0) { + LOGGER.info("{} {}s migrated", migrated, item); + } + } + LOGGER.info("Finished migration of {} {}s", uuids.size(), item); + } + + private void migrateItem(String uuid, Context context) throws SQLException { + LOGGER.debug("Migrating {} {}...", item, uuid); + + Map measureValues = new HashMap<>(); + AtomicReference componentUuid = new AtomicReference<>(null); + + MassUpdate massUpdate = context.prepareMassUpdate(); + massUpdate.select(SELECT_QUERY).setString(1, uuid); + massUpdate.update(INSERT_QUERY); + massUpdate.execute((row, update) -> { + boolean shouldUpdate = false; + String rowComponentUuid = row.getString(1); + if (componentUuid.get() == null || !rowComponentUuid.equals(componentUuid.get())) { + if (!measureValues.isEmpty()) { + preparePersistMeasure(uuid, update, componentUuid, measureValues); + shouldUpdate = true; + } + + LOGGER.debug("Starting processing of component {}...", rowComponentUuid); + componentUuid.set(rowComponentUuid); + measureValues.clear(); + readMeasureValue(row, measureValues); + } else { + readMeasureValue(row, measureValues); + } + return shouldUpdate; + }); + // insert the last component + if (!measureValues.isEmpty()) { + Upsert measureInsert = context.prepareUpsert(INSERT_QUERY); + preparePersistMeasure(uuid, measureInsert, componentUuid, measureValues); + measureInsert + .execute() + .commit(); + } + + LOGGER.debug("Flagging migration done for {} {}...", item, uuid); + + context.prepareUpsert(getUpdateFlagQuery()) + .setBoolean(1, true) + .setString(2, uuid) + .execute() + .commit(); + + LOGGER.debug("Migration finished for {} {}", item, uuid); + } + + private void preparePersistMeasure(String uuid, SqlStatement update, AtomicReference componentUuid, Map measureValues) throws SQLException { + LOGGER.debug("Persisting measures for component {}...", componentUuid.get()); + String jsonValue = GSON.toJson(measureValues); + + long jsonHash = MurmurHash3.hash128(jsonValue.getBytes(UTF_8))[0]; + + update.setString(1, componentUuid.get()); + update.setString(2, uuid); + update.setString(3, jsonValue); + update.setLong(4, jsonHash); + update.setLong(5, system2.now()); + update.setLong(6, system2.now()); + } + + private static void readMeasureValue(Select.Row row, Map measureValues) throws SQLException { + String metricName = row.getString(2); + String valueType = row.getString(3); + Double numericValue = row.getDouble(4); + String textValue = row.getString(5); + byte[] data = row.getBytes(6); + + Object metricValue = getMetricValue(data, textValue, valueType, numericValue); + if (metricValue != null) { + measureValues.put(metricName, metricValue); + } + } + + private static Object getMetricValue(@Nullable byte[] data, @Nullable String textValue, String valueType, Double numericValue) { + return TEXT_VALUE_TYPES.contains(valueType) ? getTextValue(data, textValue) : numericValue; + } + + private static String getTextValue(@Nullable byte[] data, @Nullable String textValue) { + return data != null ? new String(data, UTF_8) : textValue; + } +} diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/adhoc/MigrateBranchesLiveMeasuresToMeasures.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/adhoc/MigrateBranchesLiveMeasuresToMeasures.java new file mode 100644 index 00000000000..3138470dcb2 --- /dev/null +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/adhoc/MigrateBranchesLiveMeasuresToMeasures.java @@ -0,0 +1,30 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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.adhoc; + +import org.sonar.api.utils.System2; +import org.sonar.db.Database; + +public class MigrateBranchesLiveMeasuresToMeasures extends AbstractMigrateLiveMeasuresToMeasures { + + public MigrateBranchesLiveMeasuresToMeasures(Database db, System2 system2) { + super(db, system2, "project_branches", "branch"); + } +} diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/adhoc/MigratePortfoliosLiveMeasuresToMeasures.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/adhoc/MigratePortfoliosLiveMeasuresToMeasures.java new file mode 100644 index 00000000000..7ce171755f2 --- /dev/null +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/adhoc/MigratePortfoliosLiveMeasuresToMeasures.java @@ -0,0 +1,30 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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.adhoc; + +import org.sonar.api.utils.System2; +import org.sonar.db.Database; + +public class MigratePortfoliosLiveMeasuresToMeasures extends AbstractMigrateLiveMeasuresToMeasures { + + protected MigratePortfoliosLiveMeasuresToMeasures(Database db, System2 system2) { + super(db, system2, "portfolios", "portfolio"); + } +} 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 01380acffc0..216ba7a2cb0 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 @@ -26,7 +26,7 @@ import org.sonar.db.dialect.Dialect; public abstract class DataChange implements MigrationStep { - private final Database db; + protected final Database db; public DataChange(Database db) { this.db = db; @@ -56,7 +56,7 @@ public abstract class DataChange implements MigrationStep { return connection; } - private Connection createDdlConnection() throws SQLException { + protected Connection createDdlConnection() throws SQLException { Connection res = db.getDataSource().getConnection(); res.setAutoCommit(false); return res; diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/adhoc/MigrateBranchesLiveMeasuresToMeasuresTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/adhoc/MigrateBranchesLiveMeasuresToMeasuresTest.java new file mode 100644 index 00000000000..b7a251c6ce6 --- /dev/null +++ b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/adhoc/MigrateBranchesLiveMeasuresToMeasuresTest.java @@ -0,0 +1,180 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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.adhoc; + +import java.nio.charset.StandardCharsets; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.utils.System2; +import org.sonar.core.util.SequenceUuidFactory; +import org.sonar.db.CoreDbTester; + +import static java.lang.String.format; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.Mockito.mock; + +public class MigrateBranchesLiveMeasuresToMeasuresTest { + + private static final String MEASURES_MIGRATED_COLUMN = "measures_migrated"; + public static final String SELECT_MEASURE = """ + select component_uuid as "COMPONENT_UUID", + branch_uuid as "BRANCH_UUID", + json_value as "JSON_VALUE", + json_value_hash as "JSON_VALUE_HASH", + created_at as "CREATED_AT", + updated_at as "UPDATED_AT", + from measures + where component_uuid = '%s'"""; + + @Rule + public final CoreDbTester db = CoreDbTester.createForSchema(MigrateBranchesLiveMeasuresToMeasuresTest.class, "schema.sql"); + + private final SequenceUuidFactory uuidFactory = new SequenceUuidFactory(); + private final System2 system2 = mock(); + private final MigrateBranchesLiveMeasuresToMeasures underTest = new MigrateBranchesLiveMeasuresToMeasures(db.database(), system2); + + @Test + public void shall_do_nothing_when_called_by_execute() { + assertThatCode(underTest::execute) + .doesNotThrowAnyException(); + + assertThat(db.countRowsOfTable("measures")).isZero(); + } + + @Test + public void shall_complete_when_tables_are_empty() throws SQLException { + underTest.migrate(List.of("unused")); + + assertThat(db.countRowsOfTable("measures")).isZero(); + } + + @Test + public void should_flag_branch_with_no_measures() throws SQLException { + String branch = "branch_3"; + insertNotMigratedBranch(branch); + + underTest.migrate(List.of(branch)); + + assertBranchMigrated(branch); + assertThat(db.countRowsOfTable("measures")).isZero(); + } + + @Test + public void should_migrate_branch_with_measures() throws SQLException { + String nclocMetricUuid = insertMetric("ncloc", "INT"); + String qgStatusMetricUuid = insertMetric("quality_gate_status", "STRING"); + String metricWithDataUuid = insertMetric("metric_with_data", "DATA"); + + String branch1 = "branch_4"; + insertNotMigratedBranch(branch1); + String component1 = uuidFactory.create(); + String component2 = uuidFactory.create(); + insertMeasure(branch1, component1, nclocMetricUuid, Map.of("value", 120)); + insertMeasure(branch1, component1, qgStatusMetricUuid, Map.of("text_value", "ok")); + insertMeasure(branch1, component2, metricWithDataUuid, Map.of("measure_data", "some data".getBytes(StandardCharsets.UTF_8))); + + String branch2 = "branch_5"; + insertNotMigratedBranch(branch2); + insertMeasure(branch2, nclocMetricUuid, Map.of("value", 64)); + + String unusedBranch = "branch_6"; + insertNotMigratedBranch(unusedBranch); + insertMeasure(unusedBranch, nclocMetricUuid, Map.of("value", 3684)); + + underTest.migrate(List.of(branch1, branch2)); + + assertBranchMigrated(branch1); + assertBranchMigrated(branch2); + assertBranchNotMigrated(unusedBranch); + assertThat(db.countRowsOfTable("measures")).isEqualTo(3); + + assertThat(db.select(format(SELECT_MEASURE, component1))) + .hasSize(1) + .extracting(t -> t.get("COMPONENT_UUID"), t -> t.get("BRANCH_UUID"), t -> t.get("JSON_VALUE"), t -> t.get("JSON_VALUE_HASH")) + .containsOnly(tuple(component1, branch1, "{\"ncloc\":120.0,\"quality_gate_status\":\"ok\"}", 6033012287291512746L)); + + assertThat(db.select(format(SELECT_MEASURE, component2))) + .hasSize(1) + .extracting(t -> t.get("COMPONENT_UUID"), t -> t.get("BRANCH_UUID"), t -> t.get("JSON_VALUE"), t -> t.get("JSON_VALUE_HASH")) + .containsOnly(tuple(component2, branch1, "{\"metric_with_data\":\"some data\"}", -4524184678167636687L)); + } + + private void assertBranchMigrated(String branch) { + assertMigrationStatus(branch, true); + } + + private void assertBranchNotMigrated(String branch) { + assertMigrationStatus(branch, false); + } + + private void assertMigrationStatus(String branch, boolean expected) { + List> result = db.select(format("select %s as \"MIGRATED\" from project_branches where uuid = '%s'", MEASURES_MIGRATED_COLUMN, branch)); + assertThat(result) + .hasSize(1) + .extracting(t -> t.get("MIGRATED")) + .containsOnly(expected); + } + + private String insertMetric(String metricName, String valueType) { + String metricUuid = uuidFactory.create(); + db.executeInsert("metrics", + "uuid", metricUuid, + "name", metricName, + "val_type", valueType); + return metricUuid; + } + + private void insertMeasure(String branchUuid, String metricUuid, Map data) { + insertMeasure(branchUuid, uuidFactory.create(), metricUuid, data); + } + + private void insertMeasure(String branchUuid, String componentUuid, String metricUuid, Map data) { + Map dataMap = new HashMap<>(data); + dataMap.put("uuid", uuidFactory.create()); + dataMap.put("component_uuid", componentUuid); + dataMap.put("project_uuid", branchUuid); + dataMap.put("metric_uuid", metricUuid); + dataMap.put("created_at", 12L); + dataMap.put("updated_at", 12L); + + db.executeInsert("live_measures", dataMap); + } + + private void insertNotMigratedBranch(String branchUuid) { + db.executeInsert("project_branches", + "uuid", branchUuid, + "kee", branchUuid, + "branch_type", "LONG", + "project_uuid", uuidFactory.create(), + MEASURES_MIGRATED_COLUMN, false, + "need_issue_sync", false, + "created_at", 12L, + "updated_at", 12L + ); + } + + +} diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/adhoc/MigratePortfoliosLiveMeasuresToMeasuresTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/adhoc/MigratePortfoliosLiveMeasuresToMeasuresTest.java new file mode 100644 index 00000000000..dd76bd3d735 --- /dev/null +++ b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/adhoc/MigratePortfoliosLiveMeasuresToMeasuresTest.java @@ -0,0 +1,178 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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.adhoc; + +import java.nio.charset.StandardCharsets; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.utils.System2; +import org.sonar.core.util.SequenceUuidFactory; +import org.sonar.db.CoreDbTester; + +import static java.lang.String.format; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.Mockito.mock; + +public class MigratePortfoliosLiveMeasuresToMeasuresTest { + + private static final String MEASURES_MIGRATED_COLUMN = "measures_migrated"; + public static final String SELECT_MEASURE = """ + select component_uuid as "COMPONENT_UUID", + branch_uuid as "BRANCH_UUID", + json_value as "JSON_VALUE", + json_value_hash as "JSON_VALUE_HASH", + created_at as "CREATED_AT", + updated_at as "UPDATED_AT", + from measures where component_uuid = '%s'"""; + + @Rule + public final CoreDbTester db = CoreDbTester.createForSchema(MigratePortfoliosLiveMeasuresToMeasuresTest.class, "schema.sql"); + + private final SequenceUuidFactory uuidFactory = new SequenceUuidFactory(); + private final System2 system2 = mock(); + private final MigratePortfoliosLiveMeasuresToMeasures underTest = new MigratePortfoliosLiveMeasuresToMeasures(db.database(), system2); + + @Test + public void shall_do_nothing_when_called_by_execute() { + assertThatCode(underTest::execute) + .doesNotThrowAnyException(); + + assertThat(db.countRowsOfTable("measures")).isZero(); + } + + @Test + public void shall_complete_when_tables_are_empty() throws SQLException { + underTest.migrate(List.of("unused")); + + assertThat(db.countRowsOfTable("measures")).isZero(); + } + + @Test + public void should_flag_portfolio_with_no_measures() throws SQLException { + String portfolio = "portfolio_3"; + insertNotMigratedPortfolio(portfolio); + + underTest.migrate(List.of(portfolio)); + + assertPortfolioMigrated(portfolio); + assertThat(db.countRowsOfTable("measures")).isZero(); + } + + @Test + public void should_migrate_portfolio_with_measures() throws SQLException { + String nclocMetricUuid = insertMetric("ncloc", "INT"); + String qgStatusMetricUuid = insertMetric("quality_gate_status", "STRING"); + String metricWithDataUuid = insertMetric("metric_with_data", "DATA"); + + String portfolio1 = "portfolio_4"; + insertNotMigratedPortfolio(portfolio1); + String component1 = uuidFactory.create(); + String component2 = uuidFactory.create(); + insertMeasure(portfolio1, component1, nclocMetricUuid, Map.of("value", 120)); + insertMeasure(portfolio1, component1, qgStatusMetricUuid, Map.of("text_value", "ok")); + insertMeasure(portfolio1, component2, metricWithDataUuid, Map.of("measure_data", "some data".getBytes(StandardCharsets.UTF_8))); + + String portfolio2 = "portfolio_5"; + insertNotMigratedPortfolio(portfolio2); + insertMeasure(portfolio2, nclocMetricUuid, Map.of("value", 64)); + + String unusedPortfolio = "portfolio_6"; + insertNotMigratedPortfolio(unusedPortfolio); + insertMeasure(unusedPortfolio, nclocMetricUuid, Map.of("value", 3684)); + + underTest.migrate(List.of(portfolio1, portfolio2)); + + assertPortfolioMigrated(portfolio1); + assertPortfolioMigrated(portfolio2); + assertPortfolioNotMigrated(unusedPortfolio); + assertThat(db.countRowsOfTable("measures")).isEqualTo(3); + + assertThat(db.select(format(SELECT_MEASURE, component1))) + .hasSize(1) + .extracting(t -> t.get("COMPONENT_UUID"), t -> t.get("BRANCH_UUID"), t -> t.get("JSON_VALUE"), t -> t.get("JSON_VALUE_HASH")) + .containsOnly(tuple(component1, portfolio1, "{\"ncloc\":120.0,\"quality_gate_status\":\"ok\"}", 6033012287291512746L)); + + assertThat(db.select(format(SELECT_MEASURE, component2))) + .hasSize(1) + .extracting(t -> t.get("COMPONENT_UUID"), t -> t.get("BRANCH_UUID"), t -> t.get("JSON_VALUE"), t -> t.get("JSON_VALUE_HASH")) + .containsOnly(tuple(component2, portfolio1, "{\"metric_with_data\":\"some data\"}", -4524184678167636687L)); + } + + private void assertPortfolioMigrated(String portfolio) { + assertMigrationStatus(portfolio, true); + } + + private void assertPortfolioNotMigrated(String portfolio) { + assertMigrationStatus(portfolio, false); + } + + private void assertMigrationStatus(String portfolio, boolean expected) { + List> result = db.select(format("select %s as \"MIGRATED\" from portfolios where uuid = '%s'", MEASURES_MIGRATED_COLUMN, portfolio)); + assertThat(result) + .hasSize(1) + .extracting(t -> t.get("MIGRATED")) + .containsOnly(expected); + } + + private String insertMetric(String metricName, String valueType) { + String metricUuid = uuidFactory.create(); + db.executeInsert("metrics", + "uuid", metricUuid, + "name", metricName, + "val_type", valueType); + return metricUuid; + } + + private void insertMeasure(String portfolioUuid, String metricUuid, Map data) { + insertMeasure(portfolioUuid, uuidFactory.create(), metricUuid, data); + } + + private void insertMeasure(String portfolioUuid, String componentUuid, String metricUuid, Map data) { + Map dataMap = new HashMap<>(data); + dataMap.put("uuid", uuidFactory.create()); + dataMap.put("component_uuid", componentUuid); + dataMap.put("project_uuid", portfolioUuid); + dataMap.put("metric_uuid", metricUuid); + dataMap.put("created_at", 12L); + dataMap.put("updated_at", 12L); + + db.executeInsert("live_measures", dataMap); + } + + private void insertNotMigratedPortfolio(String portfolioUuid) { + db.executeInsert("portfolios", + "uuid", portfolioUuid, + "kee", portfolioUuid, + "name", portfolioUuid, + "private", true, + "root_uuid", portfolioUuid, + "selection_mode", "MANUAL", + MEASURES_MIGRATED_COLUMN, false, + "created_at", 12L, + "updated_at", 12L + ); + } +} diff --git a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/adhoc/MigrateBranchesLiveMeasuresToMeasuresTest/schema.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/adhoc/MigrateBranchesLiveMeasuresToMeasuresTest/schema.sql new file mode 100644 index 00000000000..4d3f9e76ad5 --- /dev/null +++ b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/adhoc/MigrateBranchesLiveMeasuresToMeasuresTest/schema.sql @@ -0,0 +1,63 @@ +CREATE TABLE "PROJECT_BRANCHES"( + "UUID" CHARACTER VARYING(50) NOT NULL, + "PROJECT_UUID" CHARACTER VARYING(50) NOT NULL, + "KEE" CHARACTER VARYING(255) NOT NULL, + "BRANCH_TYPE" CHARACTER VARYING(12) NOT NULL, + "MERGE_BRANCH_UUID" CHARACTER VARYING(50), + "PULL_REQUEST_BINARY" BINARY LARGE OBJECT, + "MANUAL_BASELINE_ANALYSIS_UUID" CHARACTER VARYING(40), + "CREATED_AT" BIGINT NOT NULL, + "UPDATED_AT" BIGINT NOT NULL, + "EXCLUDE_FROM_PURGE" BOOLEAN DEFAULT FALSE NOT NULL, + "NEED_ISSUE_SYNC" BOOLEAN NOT NULL, + "MEASURES_MIGRATED" BOOLEAN DEFAULT FALSE NOT NULL +); +ALTER TABLE "PROJECT_BRANCHES" ADD CONSTRAINT "PK_PROJECT_BRANCHES" PRIMARY KEY("UUID"); +CREATE UNIQUE INDEX "UNIQ_PROJECT_BRANCHES" ON "PROJECT_BRANCHES"("BRANCH_TYPE" NULLS FIRST, "PROJECT_UUID" NULLS FIRST, "KEE" NULLS FIRST); + +CREATE TABLE "METRICS"( + "UUID" CHARACTER VARYING(40) NOT NULL, + "NAME" CHARACTER VARYING(64) NOT NULL, + "DESCRIPTION" CHARACTER VARYING(255), + "DIRECTION" INTEGER DEFAULT 0 NOT NULL, + "DOMAIN" CHARACTER VARYING(64), + "SHORT_NAME" CHARACTER VARYING(64), + "QUALITATIVE" BOOLEAN DEFAULT FALSE NOT NULL, + "VAL_TYPE" CHARACTER VARYING(8), + "ENABLED" BOOLEAN DEFAULT TRUE, + "WORST_VALUE" DOUBLE PRECISION, + "BEST_VALUE" DOUBLE PRECISION, + "OPTIMIZED_BEST_VALUE" BOOLEAN, + "HIDDEN" BOOLEAN, + "DELETE_HISTORICAL_DATA" BOOLEAN, + "DECIMAL_SCALE" INTEGER +); +ALTER TABLE "METRICS" ADD CONSTRAINT "PK_METRICS" PRIMARY KEY("UUID"); +CREATE UNIQUE INDEX "METRICS_UNIQUE_NAME" ON "METRICS"("NAME" NULLS FIRST); + +CREATE TABLE "LIVE_MEASURES"( + "UUID" CHARACTER VARYING(40) NOT NULL, + "PROJECT_UUID" CHARACTER VARYING(50) NOT NULL, + "COMPONENT_UUID" CHARACTER VARYING(50) NOT NULL, + "METRIC_UUID" CHARACTER VARYING(40) NOT NULL, + "VALUE" DOUBLE PRECISION, + "TEXT_VALUE" CHARACTER VARYING(4000), + "MEASURE_DATA" BINARY LARGE OBJECT, + "UPDATE_MARKER" CHARACTER VARYING(40), + "CREATED_AT" BIGINT NOT NULL, + "UPDATED_AT" BIGINT NOT NULL +); +ALTER TABLE "LIVE_MEASURES" ADD CONSTRAINT "PK_LIVE_MEASURES" PRIMARY KEY("UUID"); +CREATE INDEX "LIVE_MEASURES_PROJECT" ON "LIVE_MEASURES"("PROJECT_UUID" NULLS FIRST); +CREATE UNIQUE INDEX "LIVE_MEASURES_COMPONENT" ON "LIVE_MEASURES"("COMPONENT_UUID" NULLS FIRST, "METRIC_UUID" NULLS FIRST); + +CREATE TABLE "MEASURES"( + "COMPONENT_UUID" CHARACTER VARYING(40) NOT NULL, + "BRANCH_UUID" CHARACTER VARYING(40) NOT NULL, + "JSON_VALUE" CHARACTER LARGE OBJECT NOT NULL, + "JSON_VALUE_HASH" BIGINT NOT NULL, + "CREATED_AT" BIGINT NOT NULL, + "UPDATED_AT" BIGINT NOT NULL +); +ALTER TABLE "MEASURES" ADD CONSTRAINT "PK_MEASURES" PRIMARY KEY("COMPONENT_UUID"); +CREATE INDEX "MEASURES_BRANCH_UUID" ON "MEASURES"("BRANCH_UUID" NULLS FIRST); diff --git a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/adhoc/MigratePortfoliosLiveMeasuresToMeasuresTest/schema.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/adhoc/MigratePortfoliosLiveMeasuresToMeasuresTest/schema.sql new file mode 100644 index 00000000000..2b4deb7f118 --- /dev/null +++ b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/adhoc/MigratePortfoliosLiveMeasuresToMeasuresTest/schema.sql @@ -0,0 +1,64 @@ +CREATE TABLE "PORTFOLIOS"( + "UUID" CHARACTER VARYING(40) NOT NULL, + "KEE" CHARACTER VARYING(400) NOT NULL, + "NAME" CHARACTER VARYING(2000) NOT NULL, + "DESCRIPTION" CHARACTER VARYING(2000), + "ROOT_UUID" CHARACTER VARYING(40) NOT NULL, + "PARENT_UUID" CHARACTER VARYING(40), + "PRIVATE" BOOLEAN NOT NULL, + "SELECTION_MODE" CHARACTER VARYING(50) NOT NULL, + "SELECTION_EXPRESSION" CHARACTER VARYING(4000), + "CREATED_AT" BIGINT NOT NULL, + "UPDATED_AT" BIGINT NOT NULL, + "BRANCH_KEY" CHARACTER VARYING(255), + "MEASURES_MIGRATED" BOOLEAN DEFAULT FALSE NOT NULL +); +ALTER TABLE "PORTFOLIOS" ADD CONSTRAINT "PK_PORTFOLIOS" PRIMARY KEY("UUID"); +CREATE UNIQUE INDEX "UNIQ_PORTFOLIOS_KEE" ON "PORTFOLIOS"("KEE" NULLS FIRST); + +CREATE TABLE "METRICS"( + "UUID" CHARACTER VARYING(40) NOT NULL, + "NAME" CHARACTER VARYING(64) NOT NULL, + "DESCRIPTION" CHARACTER VARYING(255), + "DIRECTION" INTEGER DEFAULT 0 NOT NULL, + "DOMAIN" CHARACTER VARYING(64), + "SHORT_NAME" CHARACTER VARYING(64), + "QUALITATIVE" BOOLEAN DEFAULT FALSE NOT NULL, + "VAL_TYPE" CHARACTER VARYING(8), + "ENABLED" BOOLEAN DEFAULT TRUE, + "WORST_VALUE" DOUBLE PRECISION, + "BEST_VALUE" DOUBLE PRECISION, + "OPTIMIZED_BEST_VALUE" BOOLEAN, + "HIDDEN" BOOLEAN, + "DELETE_HISTORICAL_DATA" BOOLEAN, + "DECIMAL_SCALE" INTEGER +); +ALTER TABLE "METRICS" ADD CONSTRAINT "PK_METRICS" PRIMARY KEY("UUID"); +CREATE UNIQUE INDEX "METRICS_UNIQUE_NAME" ON "METRICS"("NAME" NULLS FIRST); + +CREATE TABLE "LIVE_MEASURES"( + "UUID" CHARACTER VARYING(40) NOT NULL, + "PROJECT_UUID" CHARACTER VARYING(50) NOT NULL, + "COMPONENT_UUID" CHARACTER VARYING(50) NOT NULL, + "METRIC_UUID" CHARACTER VARYING(40) NOT NULL, + "VALUE" DOUBLE PRECISION, + "TEXT_VALUE" CHARACTER VARYING(4000), + "MEASURE_DATA" BINARY LARGE OBJECT, + "UPDATE_MARKER" CHARACTER VARYING(40), + "CREATED_AT" BIGINT NOT NULL, + "UPDATED_AT" BIGINT NOT NULL +); +ALTER TABLE "LIVE_MEASURES" ADD CONSTRAINT "PK_LIVE_MEASURES" PRIMARY KEY("UUID"); +CREATE INDEX "LIVE_MEASURES_PROJECT" ON "LIVE_MEASURES"("PROJECT_UUID" NULLS FIRST); +CREATE UNIQUE INDEX "LIVE_MEASURES_COMPONENT" ON "LIVE_MEASURES"("COMPONENT_UUID" NULLS FIRST, "METRIC_UUID" NULLS FIRST); + +CREATE TABLE "MEASURES"( + "COMPONENT_UUID" CHARACTER VARYING(40) NOT NULL, + "BRANCH_UUID" CHARACTER VARYING(40) NOT NULL, + "JSON_VALUE" CHARACTER LARGE OBJECT NOT NULL, + "JSON_VALUE_HASH" BIGINT NOT NULL, + "CREATED_AT" BIGINT NOT NULL, + "UPDATED_AT" BIGINT NOT NULL +); +ALTER TABLE "MEASURES" ADD CONSTRAINT "PK_MEASURES" PRIMARY KEY("COMPONENT_UUID"); +CREATE INDEX "MEASURES_BRANCH_UUID" ON "MEASURES"("BRANCH_UUID" NULLS FIRST); diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/platform/ws/MigrateMeasuresAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/platform/ws/MigrateMeasuresAction.java new file mode 100644 index 00000000000..fd7d6b35357 --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/platform/ws/MigrateMeasuresAction.java @@ -0,0 +1,153 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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.ws; + +import com.google.common.io.Resources; +import java.sql.SQLException; +import java.util.List; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +import org.sonar.api.utils.text.JsonWriter; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.server.platform.db.migration.adhoc.MigrateBranchesLiveMeasuresToMeasures; +import org.sonar.server.platform.db.migration.adhoc.MigratePortfoliosLiveMeasuresToMeasures; +import org.sonar.server.user.UserSession; + +import static java.lang.String.format; +import static java.util.Optional.ofNullable; + +public class MigrateMeasuresAction implements SystemWsAction { + public static final String SYSTEM_MEASURES_MIGRATION_ENABLED = "system.measures.migration.enabled"; + public static final String PARAM_SIZE = "size"; + + private final UserSession userSession; + private final DbClient dbClient; + private final MigrateBranchesLiveMeasuresToMeasures branchesMigration; + private final MigratePortfoliosLiveMeasuresToMeasures portfoliosMigration; + + public MigrateMeasuresAction(UserSession userSession, DbClient dbClient, + MigrateBranchesLiveMeasuresToMeasures branchesMigration, MigratePortfoliosLiveMeasuresToMeasures portfoliosMigration) { + this.userSession = userSession; + this.dbClient = dbClient; + this.branchesMigration = branchesMigration; + this.portfoliosMigration = portfoliosMigration; + } + + @Override + public void define(WebService.NewController controller) { + WebService.NewAction action = controller.createAction("migrate_measures") + .setDescription("Prepare the migration to the next major version of SonarQube." + + "
" + + "Sending a POST request to this URL will migrate some rows from the 'live_measures' to the 'measures' table. " + + "Requires system administration permission.") + .setSince("9.9.8") + .setPost(true) + .setHandler(this) + .setInternal(true) + .setResponseExample(Resources.getResource(this.getClass(), "example-migrate_measures.json")); + + action.createParam(PARAM_SIZE) + .setDescription("The number of branches or portfolios to migrate") + .setDefaultValue(10); + } + + @Override + public void handle(Request request, Response response) throws Exception { + userSession.checkIsSystemAdministrator(); + + if (!isMigrationEnabled()) { + throw new IllegalStateException("Migration is not enabled. Please call the endpoint /api/system/prepare_migration?enable=true and retry."); + } + + int size = request.mandatoryParamAsInt(PARAM_SIZE); + if (size <= 0) { + throw new IllegalArgumentException("Size must be greater than 0"); + } + + int migratedItems = migrateBranches(size); + if (migratedItems < size) { + int remainingSize = size - migratedItems; + migratedItems += migratePortfolios(remainingSize); + } + + BranchStats statistics = getStatistics(); + try (JsonWriter json = response.newJsonWriter()) { + json.beginObject() + .prop("status", "success") + .prop("message", format("%s branches or portfolios migrated", migratedItems)) + .prop("remainingBranches", statistics.remainingBranches) + .prop("totalBranches", statistics.totalBranches) + .prop("remainingPortfolios", statistics.remainingPortfolios) + .prop("totalPortfolios", statistics.totalPortfolios) + .endObject(); + } + } + + private int migrateBranches(int size) throws SQLException { + List branchesToMigrate = getBranchesToMigrate(size); + if (!branchesToMigrate.isEmpty()) { + branchesMigration.migrate(branchesToMigrate); + } + return branchesToMigrate.size(); + } + + private List getBranchesToMigrate(int size) { + try (DbSession dbSession = dbClient.openSession(false)) { + return dbClient.branchDao().selectUuidsWithMeasuresMigratedFalse(dbSession, size); + } + } + + private int migratePortfolios(int size) throws SQLException { + List portfoliosToMigrate = getPortfoliosToMigrate(size); + if (!portfoliosToMigrate.isEmpty()) { + portfoliosMigration.migrate(portfoliosToMigrate); + } + return portfoliosToMigrate.size(); + } + + private List getPortfoliosToMigrate(int size) { + try (DbSession dbSession = dbClient.openSession(false)) { + return dbClient.portfolioDao().selectUuidsWithMeasuresMigratedFalse(dbSession, size); + } + } + + private boolean isMigrationEnabled() { + return ofNullable(dbClient.propertiesDao().selectGlobalProperty(SYSTEM_MEASURES_MIGRATION_ENABLED)) + .map(p -> Boolean.parseBoolean(p.getValue())) + .orElse(false); + } + + private BranchStats getStatistics() { + try (DbSession dbSession = dbClient.openSession(false)) { + int remainingBranches = dbClient.branchDao().countByMeasuresMigratedFalse(dbSession); + int totalBranches = dbClient.branchDao().countAll(dbSession); + int remainingPortfolios = dbClient.portfolioDao().countByMeasuresMigratedFalse(dbSession); + int totalPortfolios = dbClient.portfolioDao().selectAll(dbSession).size(); + + return new BranchStats(remainingBranches, totalBranches, remainingPortfolios, totalPortfolios); + } + } + + private record BranchStats(int remainingBranches, int totalBranches, int remainingPortfolios, int totalPortfolios) { + } + +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/platform/ws/SystemWsModule.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/platform/ws/SystemWsModule.java index 5807d697a88..41c21b03174 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/platform/ws/SystemWsModule.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/platform/ws/SystemWsModule.java @@ -25,6 +25,8 @@ import org.sonar.server.platform.db.migration.adhoc.AddMeasuresMigratedColumnToP import org.sonar.server.platform.db.migration.adhoc.CreateIndexOnPortfoliosMeasuresMigrated; import org.sonar.server.platform.db.migration.adhoc.CreateIndexOnProjectBranchesMeasuresMigrated; import org.sonar.server.platform.db.migration.adhoc.CreateMeasuresTable; +import org.sonar.server.platform.db.migration.adhoc.MigrateBranchesLiveMeasuresToMeasures; +import org.sonar.server.platform.db.migration.adhoc.MigratePortfoliosLiveMeasuresToMeasures; public class SystemWsModule extends Module { @@ -47,6 +49,10 @@ public class SystemWsModule extends Module { CreateIndexOnPortfoliosMeasuresMigrated.class, PrepareMigrationAction.class, + MigrateBranchesLiveMeasuresToMeasures.class, + MigratePortfoliosLiveMeasuresToMeasures.class, + MigrateMeasuresAction.class, + InfoAction.class, LogsAction.class, MigrateDbAction.class, diff --git a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/platform/ws/example-migrate_measures.json b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/platform/ws/example-migrate_measures.json new file mode 100644 index 00000000000..60fb3154d7a --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/platform/ws/example-migrate_measures.json @@ -0,0 +1,8 @@ +{ + "status": "success", + "message": "2 branches or portfolios migrated", + "remainingBranches": 1, + "totalBranches": 3, + "remainingPortfolios": 1, + "totalPortfolios": 2 +} diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/platform/ws/MigrateMeasuresActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/platform/ws/MigrateMeasuresActionTest.java new file mode 100644 index 00000000000..a4b92076fca --- /dev/null +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/platform/ws/MigrateMeasuresActionTest.java @@ -0,0 +1,233 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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.ws; + +import com.google.gson.Gson; +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import java.sql.SQLException; +import java.util.List; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.sonar.db.Database; +import org.sonar.db.DbTester; +import org.sonar.db.component.BranchType; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.portfolio.PortfolioDto; +import org.sonar.db.property.PropertyDto; +import org.sonar.server.exceptions.ForbiddenException; +import org.sonar.server.platform.db.migration.adhoc.AddMeasuresMigratedColumnToPortfoliosTable; +import org.sonar.server.platform.db.migration.adhoc.AddMeasuresMigratedColumnToProjectBranchesTable; +import org.sonar.server.platform.db.migration.adhoc.MigrateBranchesLiveMeasuresToMeasures; +import org.sonar.server.platform.db.migration.adhoc.MigratePortfoliosLiveMeasuresToMeasures; +import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.ws.TestRequest; +import org.sonar.server.ws.TestResponse; +import org.sonar.server.ws.WsActionTester; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.sonar.db.component.BranchType.BRANCH; +import static org.sonar.server.platform.ws.MigrateMeasuresAction.SYSTEM_MEASURES_MIGRATION_ENABLED; +import static org.sonar.test.JsonAssert.assertJson; + +@RunWith(DataProviderRunner.class) +public class MigrateMeasuresActionTest { + private static final Gson GSON = new Gson(); + + public static final String PARAM_SIZE = "size"; + @Rule + public UserSessionRule userSessionRule = UserSessionRule.standalone().logIn().setSystemAdministrator(); + + @Rule + public DbTester dbTester = DbTester.create(); + + private final MigrateBranchesLiveMeasuresToMeasures measuresMigration = mock(); + private final MigratePortfoliosLiveMeasuresToMeasures portfoliosMigration = mock(); + private final MigrateMeasuresAction underTest = new MigrateMeasuresAction(userSessionRule, dbTester.getDbClient(), measuresMigration, portfoliosMigration); + private final WsActionTester tester = new WsActionTester(underTest); + + @Test + public void should_throw_if_migration_is_not_enabled() { + TestRequest request = tester.newRequest(); + + assertThatIllegalStateException() + .isThrownBy(request::execute) + .withMessage("Migration is not enabled. Please call the endpoint /api/system/prepare_migration?enable=true and retry."); + } + + @Test + @DataProvider(value = {"0", "-1", "-100"}) + public void should_throws_IAE_if_size_in_invalid(int size) throws SQLException { + enableMigration(); + + TestRequest request = tester + .newRequest() + .setParam(PARAM_SIZE, Integer.toString(size)); + + assertThatIllegalArgumentException() + .isThrownBy(request::execute) + .withMessage("Size must be greater than 0"); + } + + @Test + public void verify_example() throws SQLException { + enableMigration(); + // 3 branches, 2 migrated + ComponentDto project = dbTester.components().insertPrivateProject(); + ComponentDto branch1 = dbTester.components().insertProjectBranch(project, b -> b.setBranchType(BRANCH)); + dbTester.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.BRANCH)); + dbTester.getDbClient().branchDao().updateMeasuresMigrated(dbTester.getSession(), project.branchUuid(), true); + dbTester.getDbClient().branchDao().updateMeasuresMigrated(dbTester.getSession(), branch1.branchUuid(), true); + // 2 portfolios, 1 migrated + PortfolioDto portfolio1 = dbTester.components().insertPrivatePortfolioDto("name1"); + dbTester.components().insertPrivatePortfolioDto("name2"); + dbTester.getDbClient().portfolioDao().updateMeasuresMigrated(dbTester.getSession(), portfolio1.getUuid(), true); + dbTester.getSession().commit(); + + TestResponse response = tester.newRequest() + .execute(); + + assertJson(response.getInput()).isSimilarTo(getClass().getResource("example-migrate_measures.json")); + } + + @Test + public void does_not_migrate_portfolios_if_measures_are_not_finished() throws SQLException { + enableMigration(); + // 2 branches + ComponentDto project = dbTester.components().insertPrivateProject(); + ComponentDto branch = dbTester.components().insertProjectBranch(project, b -> b.setBranchType(BRANCH)); + dbTester.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.BRANCH)); + + TestResponse response = tester.newRequest() + .setParam(PARAM_SIZE, "2") + .execute(); + + assertThat(GSON.fromJson(response.getInput(), ActionResponse.class)) + .isEqualTo(new ActionResponse("success", "2 branches or portfolios migrated", 3, 3, 0, 0)); + verify(measuresMigration).migrate(List.of(project.uuid(), branch.uuid())); + verifyNoInteractions(portfoliosMigration); + } + + @Test + public void migrate_portfolios_to_reach_the_requested_size() throws SQLException { + enableMigration(); + + // 1 branch + ComponentDto project = dbTester.components().insertPrivateProject(); + // 2 portfolios + PortfolioDto portfolio1 = dbTester.components().insertPrivatePortfolioDto("name1"); + dbTester.components().insertPrivatePortfolioDto("name2"); + + TestResponse response = tester.newRequest() + .setParam(PARAM_SIZE, "2") + .execute(); + + assertThat(GSON.fromJson(response.getInput(), ActionResponse.class)) + .isEqualTo(new ActionResponse("success", "2 branches or portfolios migrated", 1, 1, 2, 2)); + verify(measuresMigration).migrate(List.of(project.uuid())); + verify(portfoliosMigration).migrate(List.of(portfolio1.getUuid())); + } + + @Test + public void migrate_portfolios_only_if_measures_are_done() throws SQLException { + enableMigration(); + // 2 branches, all migrated + ComponentDto project = dbTester.components().insertPrivateProject(); + ComponentDto branch1 = dbTester.components().insertProjectBranch(project, b -> b.setBranchType(BRANCH)); + dbTester.getDbClient().branchDao().updateMeasuresMigrated(dbTester.getSession(), project.branchUuid(), true); + dbTester.getDbClient().branchDao().updateMeasuresMigrated(dbTester.getSession(), branch1.branchUuid(), true); + // 2 portfolios, 1 migrated + PortfolioDto portfolio1 = dbTester.components().insertPrivatePortfolioDto("name1"); + PortfolioDto portfolio2 = dbTester.components().insertPrivatePortfolioDto("name2"); + dbTester.getDbClient().portfolioDao().updateMeasuresMigrated(dbTester.getSession(), portfolio1.getUuid(), true); + dbTester.commit(); + + TestResponse response = tester.newRequest() + .setParam(PARAM_SIZE, "2") + .execute(); + + assertThat(GSON.fromJson(response.getInput(), ActionResponse.class)) + .isEqualTo(new ActionResponse("success", "1 branches or portfolios migrated", 0, 2, 1, 2)); + verifyNoInteractions(measuresMigration); + verify(portfoliosMigration).migrate(List.of(portfolio2.getUuid())); + } + + @Test + public void does_nothing_if_migration_is_finished() throws SQLException { + enableMigration(); + // 2 branches, all migrated + ComponentDto project = dbTester.components().insertPrivateProject(); + ComponentDto branch1 = dbTester.components().insertProjectBranch(project, b -> b.setBranchType(BRANCH)); + dbTester.getDbClient().branchDao().updateMeasuresMigrated(dbTester.getSession(), project.branchUuid(), true); + dbTester.getDbClient().branchDao().updateMeasuresMigrated(dbTester.getSession(), branch1.branchUuid(), true); + // 2 portfolios, all migrated + PortfolioDto portfolio1 = dbTester.components().insertPrivatePortfolioDto("name1"); + PortfolioDto portfolio2 = dbTester.components().insertPrivatePortfolioDto("name2"); + dbTester.getDbClient().portfolioDao().updateMeasuresMigrated(dbTester.getSession(), portfolio1.getUuid(), true); + dbTester.getDbClient().portfolioDao().updateMeasuresMigrated(dbTester.getSession(), portfolio2.getUuid(), true); + dbTester.commit(); + + TestResponse response = tester.newRequest() + .setParam(PARAM_SIZE, "2") + .execute(); + + assertThat(GSON.fromJson(response.getInput(), ActionResponse.class)) + .isEqualTo(new ActionResponse("success", "0 branches or portfolios migrated", 0, 2, 0, 2)); + verifyNoInteractions(measuresMigration, portfoliosMigration); + } + + private void enableMigration() throws SQLException { + Database database = dbTester.getDbClient().getDatabase(); + new AddMeasuresMigratedColumnToProjectBranchesTable(database).execute(); + new AddMeasuresMigratedColumnToPortfoliosTable(database).execute(); + dbTester.getDbClient().propertiesDao().saveProperty(new PropertyDto().setKey(SYSTEM_MEASURES_MIGRATION_ENABLED).setValue("true")); + } + + @Test + public void throws_ForbiddenException_if_user_is_not_logged_in() { + userSessionRule.anonymous(); + + TestRequest request = tester.newRequest(); + + assertThatExceptionOfType(ForbiddenException.class) + .isThrownBy(request::execute); + } + + @Test + public void throws_ForbiddenException_if_user_is_not_system_admin() { + userSessionRule.logIn(); + + TestRequest request = tester.newRequest(); + + assertThatExceptionOfType(ForbiddenException.class) + .isThrownBy(request::execute); + } + + private record ActionResponse(String status, String message, int remainingBranches, int totalBranches, int remainingPortfolios, + int totalPortfolios) { + } +} diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/platform/ws/SystemWsModuleTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/platform/ws/SystemWsModuleTest.java index 1249de66c19..8c7a40815c2 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/platform/ws/SystemWsModuleTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/platform/ws/SystemWsModuleTest.java @@ -29,6 +29,6 @@ public class SystemWsModuleTest { public void verify_count_of_added_components() { ListContainer container = new ListContainer(); new SystemWsModule().configure(container); - assertThat(container.getAddedObjects()).hasSize(21); + assertThat(container.getAddedObjects()).hasSize(24); } } -- 2.39.5