diff options
21 files changed, 871 insertions, 12 deletions
diff --git a/build.gradle b/build.gradle index f12d73e573c..bc0f777c2f5 100644 --- a/build.gradle +++ b/build.gradle @@ -302,7 +302,7 @@ subprojects { dependency 'com.sonarsource.security:sonar-security-php-frontend-plugin:11.2.1.37710' dependency 'com.sonarsource.security:sonar-security-plugin:11.2.1.37710' dependency 'com.sonarsource.security:sonar-security-python-frontend-plugin:11.2.1.37710' - dependency 'com.sonarsource.slang:sonar-apex-plugin:1.19.0.447' + dependency 'com.sonarsource.slang:sonar-apex-plugin:1.20.0.552' dependency 'org.sonarsource.slang:sonar-ruby-plugin:1.19.0.471' dependency 'org.sonarsource.slang:sonar-scala-plugin:1.19.0.484' dependency 'com.sonarsource.swift:sonar-swift-plugin:4.13.1.8101' @@ -443,7 +443,7 @@ subprojects { entry 'log4j-api' entry 'log4j-to-slf4j' } - dependencySet(group: 'org.apache.tomcat.embed', version: '11.0.7') { + dependencySet(group: 'org.apache.tomcat.embed', version: '11.0.8') { entry 'tomcat-embed-core' entry('tomcat-embed-jasper') { exclude 'org.eclipse.jdt.core.compiler:ecj' diff --git a/gradle.properties b/gradle.properties index 1de4c764d0a..219b7317343 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,4 +15,4 @@ elasticSearchServerVersion=8.16.3 projectType=application artifactoryUrl=https://repox.jfrog.io/repox jre_release_name=jdk-17.0.13+11 -webappVersion=2025.4.0.20378 +webappVersion=2025.4.0.20784 diff --git a/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStepIT.java b/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStepIT.java index b0aed6b3f10..d0c0c7c8c3d 100644 --- a/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStepIT.java +++ b/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStepIT.java @@ -62,7 +62,6 @@ import static org.apache.commons.lang3.RandomStringUtils.secure; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.fail; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; @@ -198,7 +197,6 @@ public class LoadPeriodsStepIT extends BaseStepTest { underTest.execute(new TestComputationStepContext()); assertPeriod(NewCodePeriodType.REFERENCE_BRANCH, newCodeReferenceBranch, null); - verify(ceTaskMessages).add(any(CeTaskMessages.Message.class)); } @Test diff --git a/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStepIT.java b/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStepIT.java new file mode 100644 index 00000000000..0ede18e3ccf --- /dev/null +++ b/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStepIT.java @@ -0,0 +1,112 @@ +/* + * SonarQube + * Copyright (C) 2009-2025 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.ce.task.projectanalysis.step; + +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolderRule; +import org.sonar.ce.task.projectanalysis.analysis.TestBranch; +import org.sonar.ce.task.projectanalysis.component.Component; +import org.sonar.ce.task.projectanalysis.component.ReportComponent; +import org.sonar.ce.task.projectanalysis.component.TreeRootHolderRule; +import org.sonar.ce.task.projectanalysis.period.Period; +import org.sonar.ce.task.projectanalysis.period.PeriodHolderRule; +import org.sonar.ce.task.projectanalysis.period.PeriodOrigin; +import org.sonar.ce.task.step.ComputationStep.Context; +import org.sonar.db.DbTester; +import org.sonar.db.component.BranchDto; +import org.sonar.db.component.ProjectData; +import org.sonar.db.newcodeperiod.NewCodePeriodDto; +import org.sonar.server.project.Project; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.Mockito.mock; +import static org.sonar.db.newcodeperiod.NewCodePeriodType.REFERENCE_BRANCH; + +class PersistReferenceBranchPeriodStepIT { + + private static final String BRANCH_NAME = "feature"; + + @RegisterExtension + private final PeriodHolderRule periodHolder = new PeriodHolderRule(); + + @RegisterExtension + private final AnalysisMetadataHolderRule analysisMetadataHolder = new AnalysisMetadataHolderRule(); + + @RegisterExtension + private final TreeRootHolderRule treeRootHolderRule = new TreeRootHolderRule(); + + @RegisterExtension + private final DbTester db = DbTester.create(); + + private final PersistReferenceBranchPeriodStep persistReferenceBranchPeriodStep = new PersistReferenceBranchPeriodStep( + periodHolder, analysisMetadataHolder, db.getDbClient(), treeRootHolderRule); + + private ProjectData projectData; + private String branchUuid; + + @BeforeEach + void setUp() { + projectData = db.components().insertPrivateProject(); + BranchDto branchDto = db.components().insertProjectBranch(projectData.getProjectDto(), branch -> branch.setKey(BRANCH_NAME)); + branchUuid = branchDto.getUuid(); + + analysisMetadataHolder.setProject(new Project(projectData.projectUuid(), projectData.projectKey(), projectData.projectKey(), null, List.of())); + analysisMetadataHolder.setBranch(new TestBranch(BRANCH_NAME)); + periodHolder.setPeriod(new Period(REFERENCE_BRANCH.name(), "main", null)); + periodHolder.setPeriodOrigin(PeriodOrigin.SCANNER); + + ReportComponent reportComponent = ReportComponent + .builder(Component.Type.PROJECT, 1) + .setUuid(branchUuid) + .setKey(branchDto.getKey()) + .build(); + treeRootHolderRule.setRoot(reportComponent); + } + + @Test + void execute_shouldPersistReferenceBranchPeriod() { + + persistReferenceBranchPeriodStep.execute(mock(Context.class)); + + NewCodePeriodDto newCodePeriodDto = db.getDbClient().newCodePeriodDao().selectByBranch(db.getSession(), projectData.projectUuid(), branchUuid) + .orElseGet(() -> fail("No new code period found for branch")); + assertThat(newCodePeriodDto.getBranchUuid()).isEqualTo(branchUuid); + assertThat(newCodePeriodDto.getType()).isEqualTo(REFERENCE_BRANCH); + assertThat(newCodePeriodDto.getValue()).isEqualTo("main"); + } + + @Test + void execute_shouldUpdateReferenceBranchPeriod() { + db.newCodePeriods().insert(projectData.projectUuid(), branchUuid, REFERENCE_BRANCH, "old_value"); + + persistReferenceBranchPeriodStep.execute(mock(Context.class)); + + NewCodePeriodDto newCodePeriodDto = db.getDbClient().newCodePeriodDao().selectByBranch(db.getSession(), projectData.projectUuid(), branchUuid) + .orElseGet(() -> fail("No new code period found for branch")); + assertThat(newCodePeriodDto.getBranchUuid()).isEqualTo(branchUuid); + assertThat(newCodePeriodDto.getType()).isEqualTo(REFERENCE_BRANCH); + assertThat(newCodePeriodDto.getValue()).isEqualTo("main"); + } + +} diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodHolder.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodHolder.java index 1705c9fb3bb..9fe7dadb9f5 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodHolder.java +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodHolder.java @@ -51,4 +51,10 @@ public interface PeriodHolder { */ Period getPeriod(); + /** + * Retrieve the context from which this period is coming from. For example, it can be coming from a scanner parameter, a global setting, etc. + * See {@link PeriodOrigin} for the possible values. + */ + PeriodOrigin getPeriodOrigin(); + } diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodHolderImpl.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodHolderImpl.java index 86ea07a18e2..2150f3f5539 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodHolderImpl.java +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodHolderImpl.java @@ -29,6 +29,7 @@ public class PeriodHolderImpl implements PeriodHolder { @CheckForNull private Period period = null; private boolean initialized = false; + private PeriodOrigin periodOrigin = null; /** * Initializes the periods in the holder. @@ -41,6 +42,10 @@ public class PeriodHolderImpl implements PeriodHolder { this.initialized = true; } + public void setPeriodOrigin(PeriodOrigin periodOrigin) { + this.periodOrigin = periodOrigin; + } + @Override public boolean hasPeriod() { checkHolderIsInitialized(); @@ -60,6 +65,11 @@ public class PeriodHolderImpl implements PeriodHolder { return period; } + @Override + public PeriodOrigin getPeriodOrigin() { + return periodOrigin; + } + private void checkHolderIsInitialized() { checkState(initialized, "Period have not been initialized yet"); } diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodOrigin.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodOrigin.java new file mode 100644 index 00000000000..6e2847fe480 --- /dev/null +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/period/PeriodOrigin.java @@ -0,0 +1,25 @@ +/* + * SonarQube + * Copyright (C) 2009-2025 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.ce.task.projectanalysis.period; + +public enum PeriodOrigin { + SCANNER, + SETTINGS +} diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStep.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStep.java index 8d9026c42af..45831d3bc6f 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStep.java +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStep.java @@ -22,13 +22,13 @@ package org.sonar.ce.task.projectanalysis.step; import java.util.Optional; import org.sonar.api.utils.System2; import org.sonar.ce.task.log.CeTaskMessages; -import org.sonar.ce.task.log.CeTaskMessages.Message; import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder; import org.sonar.ce.task.projectanalysis.component.TreeRootHolder; import org.sonar.ce.task.projectanalysis.period.NewCodePeriodResolver; import org.sonar.ce.task.projectanalysis.period.Period; import org.sonar.ce.task.projectanalysis.period.PeriodHolder; import org.sonar.ce.task.projectanalysis.period.PeriodHolderImpl; +import org.sonar.ce.task.projectanalysis.period.PeriodOrigin; import org.sonar.ce.task.step.ComputationStep; import org.sonar.db.DbClient; import org.sonar.db.DbSession; @@ -89,6 +89,8 @@ public class LoadPeriodsStep implements ComputationStep { .map(b -> new NewCodePeriodDto().setType(REFERENCE_BRANCH).setValue(b)) .orElse(null); + PeriodOrigin periodOrigin = newCodePeriod == null ? PeriodOrigin.SETTINGS : PeriodOrigin.SCANNER; + try (DbSession dbSession = dbClient.openSession(false)) { Optional<NewCodePeriodDto> branchSpecificSetting = getBranchSetting(dbSession, projectUuid, branchUuid); @@ -102,13 +104,11 @@ public class LoadPeriodsStep implements ComputationStep { periodsHolder.setPeriod(null); return; } - } else if (branchSpecificSetting.isPresent()) { - ceTaskMessages.add(new Message("A scanner parameter is defining a new code reference branch, but this conflicts with the New Code Period" - + " setting of your branch. Please check your project configuration. You should use either one or the other but not both.", system2.now())); } Period period = resolver.resolve(dbSession, branchUuid, newCodePeriod, projectVersion); periodsHolder.setPeriod(period); + periodsHolder.setPeriodOrigin(periodOrigin); } } diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStep.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStep.java new file mode 100644 index 00000000000..96f471946ae --- /dev/null +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStep.java @@ -0,0 +1,113 @@ +/* + * SonarQube + * Copyright (C) 2009-2025 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.ce.task.projectanalysis.step; + +import java.util.Objects; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder; +import org.sonar.ce.task.projectanalysis.component.TreeRootHolder; +import org.sonar.ce.task.projectanalysis.period.PeriodHolder; +import org.sonar.ce.task.projectanalysis.period.PeriodOrigin; +import org.sonar.ce.task.step.ComputationStep; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.newcodeperiod.NewCodePeriodDto; +import org.sonar.db.newcodeperiod.NewCodePeriodType; + +public class PersistReferenceBranchPeriodStep implements ComputationStep { + + private static final Logger LOGGER = LoggerFactory.getLogger(PersistReferenceBranchPeriodStep.class); + + private final PeriodHolder periodHolder; + private final AnalysisMetadataHolder analysisMetadataHolder; + private final DbClient dbClient; + private final TreeRootHolder treeRootHolder; + + public PersistReferenceBranchPeriodStep(PeriodHolder periodHolder, AnalysisMetadataHolder analysisMetadataHolder, DbClient dbClient, TreeRootHolder treeRootHolder) { + this.periodHolder = periodHolder; + this.analysisMetadataHolder = analysisMetadataHolder; + this.dbClient = dbClient; + this.treeRootHolder = treeRootHolder; + } + + @Override + public String getDescription() { + return "Persist or update reference branch new code period"; + } + + @Override + public void execute(Context context) { + if (shouldExecute()) { + executePersistPeriodStep(); + } + } + + private boolean shouldExecute() { + return analysisMetadataHolder.isBranch() && periodHolder.hasPeriod() + && periodHolder.getPeriodOrigin() == PeriodOrigin.SCANNER; + } + + void executePersistPeriodStep() { + try (DbSession dbSession = dbClient.openSession(false)) { + String projectUuid = analysisMetadataHolder.getProject().getUuid(); + String branchUuid = treeRootHolder.getRoot().getUuid(); + + dbClient.newCodePeriodDao() + .selectByBranch(dbSession, projectUuid, branchUuid) + .ifPresentOrElse( + existingNewCodePeriod -> updateNewCodePeriodIfNeeded(dbSession, existingNewCodePeriod), + () -> createNewCodePeriod(dbSession, branchUuid) + ); + } + } + + private void updateNewCodePeriodIfNeeded(DbSession dbSession, NewCodePeriodDto newCodePeriodDto) { + if (shouldUpdateNewCodePeriod(newCodePeriodDto)) { + LOGGER.debug("Updating reference branch new code period '{}' for project '{}' and branch '{}'", + periodHolder.getPeriod().getModeParameter(), analysisMetadataHolder.getProject().getName(), analysisMetadataHolder.getBranch().getName()); + newCodePeriodDto.setValue(periodHolder.getPeriod().getModeParameter()); + newCodePeriodDto.setType(NewCodePeriodType.REFERENCE_BRANCH); + dbClient.newCodePeriodDao().update(dbSession, newCodePeriodDto); + dbSession.commit(); + } + } + + private boolean shouldUpdateNewCodePeriod(NewCodePeriodDto existingNewCodePeriod) { + return existingNewCodePeriod.getType() != NewCodePeriodType.REFERENCE_BRANCH + || !Objects.equals(existingNewCodePeriod.getValue(), periodHolder.getPeriod().getModeParameter()); + } + + private void createNewCodePeriod(DbSession dbSession, String branchUuid) { + LOGGER.debug("Persisting reference branch new code period '{}' for project '{}' and branch '{}'", + periodHolder.getPeriod().getModeParameter(), analysisMetadataHolder.getProject().getName(), analysisMetadataHolder.getBranch().getName()); + dbClient.newCodePeriodDao().insert(dbSession, buildNewCodePeriodDto(branchUuid)); + dbSession.commit(); + } + + private NewCodePeriodDto buildNewCodePeriodDto(String branchUuid) { + return new NewCodePeriodDto() + .setProjectUuid(analysisMetadataHolder.getProject().getUuid()) + .setBranchUuid(branchUuid) + .setType(NewCodePeriodType.REFERENCE_BRANCH) + .setValue(periodHolder.getPeriod().getModeParameter()); + } + +} diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/ReportComputationSteps.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/ReportComputationSteps.java index 8cbcbbf421a..b121df6e657 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/ReportComputationSteps.java +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/ReportComputationSteps.java @@ -106,6 +106,7 @@ public class ReportComputationSteps extends AbstractComputationSteps { // Persist data PersistScannerAnalysisCacheStep.class, PersistComponentsStep.class, + PersistReferenceBranchPeriodStep.class, PersistAnalysisStep.class, PersistAnalysisPropertiesStep.class, PersistProjectMeasuresStep.class, diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/period/PeriodHolderRule.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/period/PeriodHolderRule.java index f47424267a9..7c9b454e8c2 100644 --- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/period/PeriodHolderRule.java +++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/period/PeriodHolderRule.java @@ -73,4 +73,14 @@ public class PeriodHolderRule implements TestRule, PeriodHolder, AfterEachCallba return delegate.getPeriod(); } + @Override + public PeriodOrigin getPeriodOrigin() { + return delegate.getPeriodOrigin(); + } + + public PeriodHolderRule setPeriodOrigin(PeriodOrigin periodOrigin) { + delegate.setPeriodOrigin(periodOrigin); + return this; + } + } diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStepTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStepTest.java new file mode 100644 index 00000000000..84c3cc6d42d --- /dev/null +++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/PersistReferenceBranchPeriodStepTest.java @@ -0,0 +1,194 @@ +/* + * SonarQube + * Copyright (C) 2009-2025 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.ce.task.projectanalysis.step; + +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.ArgumentCaptor; +import org.slf4j.event.Level; +import org.sonar.api.testfixtures.log.LogTesterJUnit5; +import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder; +import org.sonar.ce.task.projectanalysis.analysis.TestBranch; +import org.sonar.ce.task.projectanalysis.component.Component; +import org.sonar.ce.task.projectanalysis.component.TreeRootHolder; +import org.sonar.ce.task.projectanalysis.period.Period; +import org.sonar.ce.task.projectanalysis.period.PeriodHolder; +import org.sonar.ce.task.projectanalysis.period.PeriodOrigin; +import org.sonar.ce.task.step.ComputationStep; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.newcodeperiod.NewCodePeriodDao; +import org.sonar.db.newcodeperiod.NewCodePeriodDto; +import org.sonar.db.newcodeperiod.NewCodePeriodType; +import org.sonar.server.project.Project; + +import static java.util.Collections.emptyList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.slf4j.event.Level.DEBUG; + +class PersistReferenceBranchPeriodStepTest { + + private static final String MAIN_BRANCH = "main"; + private static final String FEATURE_BRANCH = "feature"; + private static final String BRANCH_UUID = "branch-uuid"; + private static final String PROJECT_NAME = "project-name"; + private static final String PROJECT_UUID = "project-uuid"; + + private final PeriodHolder periodHolder = mock(PeriodHolder.class); + + private final AnalysisMetadataHolder analysisMetadataHolder = mock(AnalysisMetadataHolder.class); + + private final DbClient dbClient = mock(DbClient.class); + + private final TreeRootHolder treeRootHolder = mock(TreeRootHolder.class); + + @RegisterExtension + private final LogTesterJUnit5 logs = new LogTesterJUnit5().setLevel(Level.DEBUG); + + private final PersistReferenceBranchPeriodStep persistReferenceBranchPeriodStep = new PersistReferenceBranchPeriodStep( + periodHolder, analysisMetadataHolder, dbClient, treeRootHolder); + + private final DbSession dbSession = mock(DbSession.class); + private final NewCodePeriodDao newCodePeriodeDao = mock(NewCodePeriodDao.class); + + private final ComputationStep.Context context = mock(ComputationStep.Context.class); + + @BeforeEach + void setUp() { + Project project = new Project(PROJECT_UUID, "project-key", PROJECT_NAME, "project-description", emptyList()); + when(analysisMetadataHolder.isBranch()).thenReturn(true); + when(analysisMetadataHolder.getProject()).thenReturn(project); + when(analysisMetadataHolder.getBranch()).thenReturn(new TestBranch(FEATURE_BRANCH)); + + when(periodHolder.hasPeriod()).thenReturn(true); + Period period = new Period(NewCodePeriodType.REFERENCE_BRANCH.name(), MAIN_BRANCH, null); + when(periodHolder.getPeriod()).thenReturn(period); + when(periodHolder.getPeriodOrigin()).thenReturn(PeriodOrigin.SCANNER); + + when(dbClient.openSession(false)).thenReturn(dbSession); + when(dbClient.newCodePeriodDao()).thenReturn(newCodePeriodeDao); + + Component root = mock(Component.class); + when(treeRootHolder.getRoot()).thenReturn(root); + when(root.getUuid()).thenReturn(BRANCH_UUID); + + } + + @Test + void getDescription() { + assertThat(persistReferenceBranchPeriodStep.getDescription()).isEqualTo("Persist or update reference branch new code period"); + } + + @Test + void execute_shouldDoNothing_whenNotABranch() { + when(analysisMetadataHolder.isBranch()).thenReturn(false); + verifyExecuteNotCalled(); + } + + @Test + void execute_shouldDoNothing_whenNoPeriods() { + when(periodHolder.hasPeriod()).thenReturn(false); + verifyExecuteNotCalled(); + } + + @Test + void execute_shouldDoNothing_whenNotReferenceBranchPeriod() { + Period period = new Period("not-ref-branch", MAIN_BRANCH, null); + when(periodHolder.getPeriod()).thenReturn(period); + when(periodHolder.getPeriodOrigin()).thenReturn(PeriodOrigin.SETTINGS); + verifyExecuteNotCalled(); + } + + private void verifyExecuteNotCalled() { + PersistReferenceBranchPeriodStep spyStep = spy(persistReferenceBranchPeriodStep); + + spyStep.execute(context); + + verify(spyStep, never()).executePersistPeriodStep(); + } + + @Test + void execute_shouldCreateNewCodePeriod_whenItDoesNotExists() { + NewCodePeriodDto expectedNewCodePeriod = new NewCodePeriodDto() + .setBranchUuid(BRANCH_UUID) + .setProjectUuid(PROJECT_UUID) + .setType(NewCodePeriodType.REFERENCE_BRANCH) + .setValue(MAIN_BRANCH); + when(newCodePeriodeDao.selectByBranch(dbSession, PROJECT_UUID, BRANCH_UUID)).thenReturn(Optional.empty()); + + persistReferenceBranchPeriodStep.execute(context); + + assertThat(logs.logs(DEBUG)).contains( + String.format("Persisting reference branch new code period '%s' for project '%s' and branch '%s'",MAIN_BRANCH, PROJECT_NAME, FEATURE_BRANCH)); + ArgumentCaptor<NewCodePeriodDto> newCodePeriodCaptor = ArgumentCaptor.forClass(NewCodePeriodDto.class); + verify(newCodePeriodeDao).insert(eq(dbSession), newCodePeriodCaptor.capture()); + assertThat(newCodePeriodCaptor.getValue()).usingRecursiveComparison().isEqualTo(expectedNewCodePeriod); + } + + @Test + void execute_shouldUpdateNewCodePeriod_whenItExistsAndItChanged() { + NewCodePeriodDto expectedNewCodePeriod = new NewCodePeriodDto() + .setBranchUuid(BRANCH_UUID) + .setProjectUuid(PROJECT_UUID) + .setType(NewCodePeriodType.REFERENCE_BRANCH) + .setValue(MAIN_BRANCH); + var newCodePeriodInBase = new NewCodePeriodDto() + .setBranchUuid(BRANCH_UUID) + .setProjectUuid(PROJECT_UUID) + .setType(NewCodePeriodType.REFERENCE_BRANCH) + .setValue("old-value"); + + when(newCodePeriodeDao.selectByBranch(dbSession, PROJECT_UUID, BRANCH_UUID)).thenReturn(Optional.of(newCodePeriodInBase)); + + persistReferenceBranchPeriodStep.execute(context); + + assertThat(logs.logs(DEBUG)).contains( + String.format("Updating reference branch new code period '%s' for project '%s' and branch '%s'", MAIN_BRANCH ,PROJECT_NAME, FEATURE_BRANCH)); + ArgumentCaptor<NewCodePeriodDto> newCodePeriodCaptor = ArgumentCaptor.forClass(NewCodePeriodDto.class); + verify(newCodePeriodeDao).update(eq(dbSession), newCodePeriodCaptor.capture()); + assertThat(newCodePeriodCaptor.getValue()).usingRecursiveComparison().isEqualTo(expectedNewCodePeriod); + } + + @Test + void execute_shouldDoNothing_whenItExistsAndItDidNotChanged() { + NewCodePeriodDto expectedNewCodePeriod = new NewCodePeriodDto() + .setBranchUuid(BRANCH_UUID) + .setProjectUuid(PROJECT_UUID) + .setType(NewCodePeriodType.REFERENCE_BRANCH) + .setValue(MAIN_BRANCH); + + when(newCodePeriodeDao.selectByBranch(dbSession, PROJECT_UUID, BRANCH_UUID)).thenReturn(Optional.of(expectedNewCodePeriod)); + + persistReferenceBranchPeriodStep.execute(context); + + verify(newCodePeriodeDao, never()).update(any(), any()); + verify(newCodePeriodeDao, never()).insert(any(), any()); + } + +} diff --git a/server/sonar-db-dao/src/it/java/org/sonar/db/purge/PurgeDaoIT.java b/server/sonar-db-dao/src/it/java/org/sonar/db/purge/PurgeDaoIT.java index b781c2ad344..f468306e8f8 100644 --- a/server/sonar-db-dao/src/it/java/org/sonar/db/purge/PurgeDaoIT.java +++ b/server/sonar-db-dao/src/it/java/org/sonar/db/purge/PurgeDaoIT.java @@ -1994,7 +1994,8 @@ oldCreationDate)); // the issue uuids here don't even exist but doesn't matter, we don't delete issues so not testing that var issueReleaseBase = Map.of("created_at", 0L, "updated_at", 0L, - "severity", "INFO", "severity_sort_key", 42, "status", "TO_REVIEW"); + "severity", "INFO", "original_severity", "INFO", "manual_severity", false, + "severity_sort_key", 42, "status", "TO_REVIEW"); db.executeInsert("sca_issues_releases", merge(issueReleaseBase, Map.of("uuid", "issue-release-uuid1", "sca_issue_uuid", "issue-uuid1", "sca_release_uuid", "release-uuid1"))); db.executeInsert("sca_issues_releases", merge(issueReleaseBase, Map.of("uuid", "issue-release-uuid2", diff --git a/server/sonar-db-dao/src/schema/schema-sq.ddl b/server/sonar-db-dao/src/schema/schema-sq.ddl index 9001f44d56f..1718ab4dddd 100644 --- a/server/sonar-db-dao/src/schema/schema-sq.ddl +++ b/server/sonar-db-dao/src/schema/schema-sq.ddl @@ -1118,7 +1118,9 @@ CREATE TABLE "SCA_ISSUES_RELEASES"( "UPDATED_AT" BIGINT NOT NULL, "STATUS" CHARACTER VARYING(40) NOT NULL, "ASSIGNEE_UUID" CHARACTER VARYING(40), - "PREVIOUS_MANUAL_STATUS" CHARACTER VARYING(40) + "PREVIOUS_MANUAL_STATUS" CHARACTER VARYING(40), + "ORIGINAL_SEVERITY" CHARACTER VARYING(15) NOT NULL, + "MANUAL_SEVERITY" CHARACTER VARYING(15) ); ALTER TABLE "SCA_ISSUES_RELEASES" ADD CONSTRAINT "PK_SCA_ISSUES_RELEASES" PRIMARY KEY("UUID"); CREATE INDEX "SCA_ISSUES_RELEASES_SCA_ISSUE" ON "SCA_ISSUES_RELEASES"("SCA_ISSUE_UUID" NULLS FIRST); diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddOriginalAndManualSeverityToScaIssuesTest.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddOriginalAndManualSeverityToScaIssuesTest.java new file mode 100644 index 00000000000..0fdbda384a9 --- /dev/null +++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/AddOriginalAndManualSeverityToScaIssuesTest.java @@ -0,0 +1,63 @@ +/* + * SonarQube + * Copyright (C) 2009-2025 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.v202504; + +import java.sql.SQLException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.sonar.db.MigrationDbTester; +import org.sonar.server.platform.db.migration.step.DdlChange; + +import static java.sql.Types.VARCHAR; +import static org.sonar.db.MigrationDbTester.createForMigrationStep; +import static org.sonar.server.platform.db.migration.version.v202504.AddOriginalAndManualSeverityToScaIssues.MANUALLY_SET_COLUMN_NAME; +import static org.sonar.server.platform.db.migration.version.v202504.AddOriginalAndManualSeverityToScaIssues.ORIGINAL_VALUE_COLUMN_NAME; +import static org.sonar.server.platform.db.migration.version.v202504.AddOriginalAndManualSeverityToScaIssues.TABLE_NAME; + +class AddOriginalAndManualSeverityToScaIssuesTest { + + @RegisterExtension + public final MigrationDbTester db = createForMigrationStep(AddOriginalAndManualSeverityToScaIssues.class); + private final DdlChange underTest = new AddOriginalAndManualSeverityToScaIssues(db.database()); + + @Test + void execute_shouldAddCalculatedValueColumn() throws SQLException { + db.assertColumnDoesNotExist(TABLE_NAME, ORIGINAL_VALUE_COLUMN_NAME); + underTest.execute(); + db.assertColumnDefinition(TABLE_NAME, ORIGINAL_VALUE_COLUMN_NAME, VARCHAR, 15, true); + } + + @Test + void execute_shouldAddManuallySetColumn() throws SQLException { + db.assertColumnDoesNotExist(TABLE_NAME, MANUALLY_SET_COLUMN_NAME); + underTest.execute(); + db.assertColumnDefinition(TABLE_NAME, MANUALLY_SET_COLUMN_NAME, VARCHAR, 15, true); + } + + @Test + void execute_shouldBeReentrant() throws SQLException { + db.assertColumnDoesNotExist(TABLE_NAME, ORIGINAL_VALUE_COLUMN_NAME); + db.assertColumnDoesNotExist(TABLE_NAME, MANUALLY_SET_COLUMN_NAME); + underTest.execute(); + underTest.execute(); + db.assertColumnDefinition(TABLE_NAME, ORIGINAL_VALUE_COLUMN_NAME, VARCHAR, 15, true); + db.assertColumnDefinition(TABLE_NAME, MANUALLY_SET_COLUMN_NAME, VARCHAR, 15, true); + } +} diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/PopulateOriginalSeverityForScaIssuesReleasesTableIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/PopulateOriginalSeverityForScaIssuesReleasesTableIT.java new file mode 100644 index 00000000000..31091c17fc1 --- /dev/null +++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/PopulateOriginalSeverityForScaIssuesReleasesTableIT.java @@ -0,0 +1,80 @@ +/* + * SonarQube + * Copyright (C) 2009-2025 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.v202504; + +import java.sql.SQLException; +import java.util.Date; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.sonar.db.MigrationDbTester; + +import static org.assertj.core.api.Assertions.assertThat; + +class PopulateOriginalSeverityForScaIssuesReleasesTableIT { + @RegisterExtension + public final MigrationDbTester db = MigrationDbTester.createForMigrationStep(PopulateOriginalSeverityForScaIssuesReleasesTable.class); + private final PopulateOriginalSeverityForScaIssuesReleasesTable underTest = new PopulateOriginalSeverityForScaIssuesReleasesTable(db.database()); + + @Test + void execute_shouldPopulateOriginaldSeverity() throws SQLException { + insertScaIssuesReleases(1, "HIGH", null); + insertScaIssuesReleases(2, "INFO", null); + + underTest.execute(); + + assertThatOriginalSeverityIs(1, "HIGH"); + assertThatOriginalSeverityIs(2, "INFO"); + } + + @Test + void execute_whenAlreadyExecuted_shouldBeIdempotent() throws SQLException { + insertScaIssuesReleases(1, "HIGH", "INFO"); + + underTest.execute(); + underTest.execute(); + + assertThatOriginalSeverityIs(1, "INFO"); + } + + private void insertScaIssuesReleases(Integer index, String severity, @Nullable String originalSeverity) { + db.executeInsert("sca_issues_releases", + "uuid", "uuid-" + index, + "sca_issue_uuid", "issue_id" + index, + "sca_release_uuid", "release_id", + "status", "TO_REVIEW", + "severity", severity, + "original_severity", originalSeverity, + "manual_severity", null, + "severity_sort_key", 1, + "created_at", new Date().getTime(), + "updated_at", new Date().getTime() + ); + } + + private void assertThatOriginalSeverityIs(Integer index, String expectedSeverity) { + String uuid = "uuid-" + index; + List<Map<String, Object>> rows = db.select("select original_severity from sca_issues_releases where uuid = '%s'".formatted(uuid)); + assertThat(rows).isNotEmpty() + .allSatisfy(row -> assertThat(row).containsEntry("original_severity", expectedSeverity)); + } +} diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/UpdateScaIssuesReleasesOriginalSeverityColumnNotNullableTestIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/UpdateScaIssuesReleasesOriginalSeverityColumnNotNullableTestIT.java new file mode 100644 index 00000000000..2e194f52ec6 --- /dev/null +++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202504/UpdateScaIssuesReleasesOriginalSeverityColumnNotNullableTestIT.java @@ -0,0 +1,68 @@ +/* + * SonarQube + * Copyright (C) 2009-2025 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.v202504; + +import java.sql.SQLException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.sonar.db.MigrationDbTester; +import org.sonar.server.platform.db.migration.sql.DropColumnsBuilder; + +import static java.sql.Types.VARCHAR; +import static org.assertj.core.api.Assertions.assertThatCode; + +class UpdateScaIssuesReleasesOriginalSeverityColumnNotNullableTestIT { + static final String TABLE_NAME = "sca_issues_releases"; + static final String COLUMN_NAME = "original_severity"; + static final int COLUMN_SIZE = 15; + + @RegisterExtension + public final MigrationDbTester db = MigrationDbTester.createForMigrationStep(UpdateScaIssuesReleasesOriginalSeverityColumnNotNullable.class); + + private final UpdateScaIssuesReleasesOriginalSeverityColumnNotNullable underTest = new UpdateScaIssuesReleasesOriginalSeverityColumnNotNullable(db.database()); + + @Test + void execute_whenColumnExists_shouldMakeColumnNotNull() throws SQLException { + // Verify column is nullable before update + db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, COLUMN_SIZE, true); + + underTest.execute(); + + // Verify column is not nullable after update + db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, COLUMN_SIZE, false); + } + + @Test + void execute_whenColumnDoesNotExist_shouldNotFail() throws SQLException { + // Ensure the column does not exist before executing the migration + DropColumnsBuilder dropColumnsBuilder = new DropColumnsBuilder(db.database().getDialect(), TABLE_NAME, COLUMN_NAME); + dropColumnsBuilder.build().forEach(db::executeDdl); + + db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME); + assertThatCode(underTest::execute).doesNotThrowAnyException(); + } + + @Test + void execute_whenExecutedTwice_shouldBeIdempotent() throws SQLException { + underTest.execute(); + assertThatCode(underTest::execute).doesNotThrowAnyException(); + db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, VARCHAR, COLUMN_SIZE, false); + } +} diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddOriginalAndManualSeverityToScaIssues.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddOriginalAndManualSeverityToScaIssues.java new file mode 100644 index 00000000000..66c4c27254e --- /dev/null +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/AddOriginalAndManualSeverityToScaIssues.java @@ -0,0 +1,67 @@ +/* + * SonarQube + * Copyright (C) 2009-2025 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.v202504; + +import java.sql.SQLException; +import org.sonar.db.Database; +import org.sonar.server.platform.db.migration.def.VarcharColumnDef; +import org.sonar.server.platform.db.migration.sql.AddColumnsBuilder; +import org.sonar.server.platform.db.migration.step.DdlChange; + +import static org.sonar.db.DatabaseUtils.tableColumnExists; + +public class AddOriginalAndManualSeverityToScaIssues extends DdlChange { + static final String TABLE_NAME = "sca_issues_releases"; + static final String ORIGINAL_VALUE_COLUMN_NAME = "original_severity"; + static final String MANUALLY_SET_COLUMN_NAME = "manual_severity"; + + public AddOriginalAndManualSeverityToScaIssues(Database db) { + super(db); + } + + @Override + public void execute(Context context) throws SQLException { + try (var connection = getDatabase().getDataSource().getConnection()) { + if (!tableColumnExists(connection, TABLE_NAME, ORIGINAL_VALUE_COLUMN_NAME)) { + var columnDef = VarcharColumnDef.newVarcharColumnDefBuilder() + .setColumnName(ORIGINAL_VALUE_COLUMN_NAME) + .setLimit(15) + .setIsNullable(true) + .build(); + + context.execute(new AddColumnsBuilder(getDialect(), TABLE_NAME) + .addColumn(columnDef) + .build()); + } + + if (!tableColumnExists(connection, TABLE_NAME, MANUALLY_SET_COLUMN_NAME)) { + var columnDef = VarcharColumnDef.newVarcharColumnDefBuilder() + .setColumnName(MANUALLY_SET_COLUMN_NAME) + .setLimit(15) + .setIsNullable(true) + .build(); + + context.execute(new AddColumnsBuilder(getDialect(), TABLE_NAME) + .addColumn(columnDef) + .build()); + } + } + } +} diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/DbVersion202504.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/DbVersion202504.java index ac608e306a5..97a956b6ca7 100644 --- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/DbVersion202504.java +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/DbVersion202504.java @@ -33,6 +33,11 @@ public class DbVersion202504 implements DbVersion { .add(2025_04_001, "Add 'organization_uuid' column to 'sca_license_profiles' table", AddOrganizationUuidToScaLicenseProfiles.class) .add(2025_04_002, "Drop unique index from 'sca_license_profiles'", DropUniqueIndexOnScaLicenseProfiles.class) .add(2025_04_003, "Create unique index from 'sca_license_profiles'", CreateUniqueIndexOnScaLicenseProfiles.class) - .add(2025_04_004, "Add index on 'alm_repo' column in 'project_alm_settings' table", AddIndexOnAlmRepoInProjectAlmSettings.class); + .add(2025_04_004, "Add index on 'alm_repo' column in 'project_alm_settings' table", AddIndexOnAlmRepoInProjectAlmSettings.class) + .add(2025_04_005, "Add 'original_severity' and 'manual_severity' columns to 'sca_issues_releases' table", AddOriginalAndManualSeverityToScaIssues.class) + .add(2025_04_006, "Populate 'original_severity' column for 'sca_issues_releases' table", PopulateOriginalSeverityForScaIssuesReleasesTable.class) + .add(2025_04_007, "Update 'original_severity' column to be not nullable in 'sca_issues_releases' table", UpdateScaIssuesReleasesOriginalSeverityColumnNotNullable.class) + + ; } } diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/PopulateOriginalSeverityForScaIssuesReleasesTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/PopulateOriginalSeverityForScaIssuesReleasesTable.java new file mode 100644 index 00000000000..bcee5bb0903 --- /dev/null +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/PopulateOriginalSeverityForScaIssuesReleasesTable.java @@ -0,0 +1,50 @@ +/* + * SonarQube + * Copyright (C) 2009-2025 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.v202504; + +import java.sql.SQLException; +import org.sonar.db.Database; +import org.sonar.server.platform.db.migration.step.DataChange; +import org.sonar.server.platform.db.migration.step.MassUpdate; + +public class PopulateOriginalSeverityForScaIssuesReleasesTable extends DataChange { + private static final String SELECT_QUERY = "select severity, uuid from sca_issues_releases where original_severity is null"; + private static final String UPDATE_QUERY = "update sca_issues_releases set original_severity = ? where uuid = ?"; + + public PopulateOriginalSeverityForScaIssuesReleasesTable(Database db) { + super(db); + } + + @Override + protected void execute(Context context) throws SQLException { + MassUpdate massUpdate = context.prepareMassUpdate(); + massUpdate.select(SELECT_QUERY); + massUpdate.update(UPDATE_QUERY); + + massUpdate.execute((row, update, index) -> { + update + // Set original_severity from severity + .setString(1, row.getString(1)) + // Set uuid from uuid + .setString(2, row.getString(2)); + return true; + }); + } +} diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/UpdateScaIssuesReleasesOriginalSeverityColumnNotNullable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/UpdateScaIssuesReleasesOriginalSeverityColumnNotNullable.java new file mode 100644 index 00000000000..4c842e81e1f --- /dev/null +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202504/UpdateScaIssuesReleasesOriginalSeverityColumnNotNullable.java @@ -0,0 +1,54 @@ +/* + * SonarQube + * Copyright (C) 2009-2025 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.v202504; + +import java.sql.SQLException; +import org.sonar.db.Database; +import org.sonar.server.platform.db.migration.def.VarcharColumnDef; +import org.sonar.server.platform.db.migration.sql.AlterColumnsBuilder; +import org.sonar.server.platform.db.migration.step.DdlChange; + +import static org.sonar.db.DatabaseUtils.tableColumnExists; + +public class UpdateScaIssuesReleasesOriginalSeverityColumnNotNullable extends DdlChange { + static final String TABLE_NAME = "sca_issues_releases"; + static final String COLUMN_NAME = "original_severity"; + + public UpdateScaIssuesReleasesOriginalSeverityColumnNotNullable(Database db) { + super(db); + } + + @Override + public void execute(Context context) throws SQLException { + try (var connection = getDatabase().getDataSource().getConnection()) { + if (tableColumnExists(connection, TABLE_NAME, COLUMN_NAME)) { + var columnDef = VarcharColumnDef.newVarcharColumnDefBuilder() + .setColumnName(COLUMN_NAME) + .setIsNullable(false) + .setLimit(15) + .build(); + + context.execute(new AlterColumnsBuilder(getDialect(), TABLE_NAME) + .updateColumn(columnDef) + .build()); + } + } + } +} |