From: Jacek Poreda Date: Thu, 20 Jul 2023 11:35:09 +0000 (+0200) Subject: SONAR-7697 Replace integer `purged_status` column with boolean `purged` X-Git-Tag: 10.2.0.77647~310 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=7a54c788fcec90568e1da456cac6f85e04b91a9d;p=sonarqube.git SONAR-7697 Replace integer `purged_status` column with boolean `purged` --- 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 ec0bb78cd87..b8182de0987 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 @@ -71,6 +71,7 @@ import org.sonar.db.component.ComponentDto; import org.sonar.db.component.ComponentTesting; import org.sonar.db.component.ProjectData; import org.sonar.db.component.SnapshotDto; +import org.sonar.db.dialect.Dialect; import org.sonar.db.event.EventComponentChangeDto; import org.sonar.db.event.EventDto; import org.sonar.db.event.EventTesting; @@ -100,7 +101,6 @@ import static java.util.Collections.singletonList; import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.groups.Tuple.tuple; -import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.only; import static org.mockito.Mockito.verify; @@ -141,7 +141,8 @@ public class PurgeDaoIT { db.components().insertSnapshot(project.getMainBranchComponent(), t -> t.setStatus(STATUS_UNPROCESSED).setLast(false)); SnapshotDto lastAnalysis = db.components().insertSnapshot(project.getMainBranchComponent(), t -> t.setStatus(STATUS_PROCESSED).setLast(true)); - underTest.purge(dbSession, newConfigurationWith30Days(System2.INSTANCE, project.getMainBranchComponent().uuid(), project.projectUuid()), PurgeListener.EMPTY, new PurgeProfiler()); + underTest.purge(dbSession, newConfigurationWith30Days(System2.INSTANCE, project.getMainBranchComponent().uuid(), project.projectUuid()), PurgeListener.EMPTY, + new PurgeProfiler()); dbSession.commit(); assertThat(uuidsOfAnalysesOfRoot(project.getMainBranchComponent())).containsOnly(pastAnalysis.getUuid(), lastAnalysis.getUuid()); @@ -198,9 +199,11 @@ public class PurgeDaoIT { BranchDto pr3 = db.components().insertProjectBranch(project.getProjectDto(), b -> b.setBranchType(BranchType.PULL_REQUEST).setExcludeFromPurge(true)); addComponentsSnapshotsAndIssuesToBranch(pr3, rule, 100); - updateBranchCreationDate(date31DaysAgo, branch1.getUuid(), branch2.getUuid(), branch3.getUuid(), branch4.getUuid(), branch5.getUuid(), pr1.getUuid(), pr2.getUuid(), pr3.getUuid()); + updateBranchCreationDate(date31DaysAgo, branch1.getUuid(), branch2.getUuid(), branch3.getUuid(), branch4.getUuid(), branch5.getUuid(), pr1.getUuid(), pr2.getUuid(), + pr3.getUuid()); - underTest.purge(dbSession, newConfigurationWith30Days(System2.INSTANCE, project.getMainBranchDto().getUuid(), project.getProjectDto().getUuid()), PurgeListener.EMPTY, new PurgeProfiler()); + underTest.purge(dbSession, newConfigurationWith30Days(System2.INSTANCE, project.getMainBranchDto().getUuid(), project.getProjectDto().getUuid()), PurgeListener.EMPTY, + new PurgeProfiler()); dbSession.commit(); assertThat(uuidsIn("components")).containsOnly( @@ -232,7 +235,8 @@ public class PurgeDaoIT { ComponentDto file = db.components().insertComponent(newFileDto(pullRequestComponent, dir)); db.issues().insert(rule, pullRequestComponent, file); - underTest.purge(dbSession, newConfigurationWith30Days(System2.INSTANCE, project.getMainBranchComponent().branchUuid(), project.getProjectDto().getUuid()), PurgeListener.EMPTY, new PurgeProfiler()); + underTest.purge(dbSession, newConfigurationWith30Days(System2.INSTANCE, project.getMainBranchComponent().branchUuid(), project.getProjectDto().getUuid()), PurgeListener.EMPTY, + new PurgeProfiler()); dbSession.commit(); assertThat(uuidsIn("components")).containsOnly(project.getMainBranchDto().getUuid(), nonMainBranch.getUuid(), recentPullRequest.getUuid()); @@ -296,7 +300,7 @@ public class PurgeDaoIT { IssueDto openOnEnabledComponent = db.issues().insert(rule, mainBranch, enabledFile, issue -> issue.setStatus("OPEN")); IssueDto confirmOnEnabledComponent = db.issues().insert(rule, mainBranch, enabledFile, issue -> issue.setStatus("CONFIRM")); - assertThat(db.countSql("select count(*) from snapshots where purge_status = 1")).isZero(); + assertThat(countPurgedSnapshots()).isZero(); assertThat(db.countSql("select count(*) from issues where status = 'CLOSED'")).isZero(); assertThat(db.countSql("select count(*) from issues where resolution = 'REMOVED'")).isZero(); @@ -324,8 +328,8 @@ public class PurgeDaoIT { purgeListener, new PurgeProfiler()); dbSession.commit(); - // set purge_status=1 for non-last snapshot - assertThat(db.countSql("select count(*) from snapshots where purge_status = 1")).isOne(); + // set purged=true for non-last snapshot + assertThat(countPurgedSnapshots()).isOne(); // close open issues of selected assertThat(db.countSql("select count(*) from issues where status = 'CLOSED'")).isEqualTo(4); @@ -696,10 +700,12 @@ public class PurgeDaoIT { underTest.deleteBranch(dbSession, appBranch.getUuid()); dbSession.commit(); - assertThat(uuidsIn("components")).containsOnly(project.getMainBranchDto().getUuid(), app.getMainBranchDto().getUuid(), otherApp.getMainBranchDto().getUuid(), otherAppBranch.getUuid()); + assertThat(uuidsIn("components")).containsOnly(project.getMainBranchDto().getUuid(), app.getMainBranchDto().getUuid(), otherApp.getMainBranchDto().getUuid(), + otherAppBranch.getUuid()); assertThat(uuidsIn("projects")).containsOnly(project.getProjectDto().getUuid(), app.getProjectDto().getUuid(), otherApp.getProjectDto().getUuid()); assertThat(uuidsIn("snapshots")).containsOnly(otherAppAnalysis.getUuid(), appAnalysis.getUuid(), otherAppBranchAnalysis.getUuid()); - assertThat(uuidsIn("project_branches")).containsOnly(project.getMainBranchDto().getUuid(), app.getMainBranchDto().getUuid(), otherApp.getMainBranchDto().getUuid(), otherAppBranch.getUuid()); + assertThat(uuidsIn("project_branches")).containsOnly(project.getMainBranchDto().getUuid(), app.getMainBranchDto().getUuid(), otherApp.getMainBranchDto().getUuid(), + otherAppBranch.getUuid()); assertThat(uuidsIn("project_measures")).containsOnly(appMeasure.getUuid(), otherAppMeasure.getUuid(), otherAppBranchMeasure.getUuid()); assertThat(uuidsIn("app_projects", "application_uuid")).containsOnly(app.getProjectDto().getUuid(), otherApp.getProjectDto().getUuid()); assertThat(uuidsIn("app_branch_project_branch", "application_branch_uuid")).containsOnly(otherAppBranch.getUuid()); @@ -775,7 +781,6 @@ public class PurgeDaoIT { ComponentDto anotherBranch = db.components().insertProjectBranch(project.getMainBranchComponent()); ProjectData anotherProject = db.components().insertPrivateProject(); - CeActivityDto projectTask = insertCeActivity(project.getMainBranchComponent(), project.getProjectDto().getUuid()); insertCeTaskInput(projectTask.getUuid()); CeActivityDto branchTask = insertCeActivity(branch, project.getProjectDto().getUuid()); @@ -793,7 +798,8 @@ public class PurgeDaoIT { assertThat(uuidsIn("ce_activity")).containsOnly(projectTask.getUuid(), anotherBranchTask.getUuid(), anotherProjectTask.getUuid()); assertThat(taskUuidsIn("ce_task_input")).containsOnly(projectTask.getUuid(), anotherBranchTask.getUuid(), anotherProjectTask.getUuid(), "non existing task"); - underTest.deleteProject(dbSession, project.getProjectDto().getUuid(), project.getProjectDto().getQualifier(), project.getProjectDto().getName(), project.getProjectDto().getKey()); + underTest.deleteProject(dbSession, project.getProjectDto().getUuid(), project.getProjectDto().getQualifier(), project.getProjectDto().getName(), + project.getProjectDto().getKey()); dbSession.commit(); assertThat(uuidsIn("ce_activity")).containsOnly(anotherProjectTask.getUuid()); @@ -824,7 +830,8 @@ public class PurgeDaoIT { assertThat(uuidsIn("ce_activity")).containsOnly(projectTask.getUuid(), anotherBranchTask.getUuid(), anotherProjectTask.getUuid()); assertThat(taskUuidsIn("ce_scanner_context")).containsOnly(projectTask.getUuid(), anotherBranchTask.getUuid(), anotherProjectTask.getUuid(), "non existing task"); - underTest.deleteProject(dbSession, project.getProjectDto().getUuid(), project.getProjectDto().getQualifier(), project.getProjectDto().getName(), project.getProjectDto().getKey()); + underTest.deleteProject(dbSession, project.getProjectDto().getUuid(), project.getProjectDto().getQualifier(), project.getProjectDto().getName(), + project.getProjectDto().getKey()); dbSession.commit(); assertThat(uuidsIn("ce_activity")).containsOnly(anotherProjectTask.getUuid()); @@ -838,7 +845,6 @@ public class PurgeDaoIT { ComponentDto anotherBranch = db.components().insertProjectBranch(project.getMainBranchComponent()); ProjectData anotherProject = db.components().insertPrivateProject(); - CeActivityDto projectTask = insertCeActivity(project.getMainBranchComponent(), project.projectUuid()); insertCeTaskCharacteristics(projectTask.getUuid(), 3); CeActivityDto branchTask = insertCeActivity(branch, project.projectUuid()); @@ -856,7 +862,8 @@ public class PurgeDaoIT { assertThat(uuidsIn("ce_activity")).containsOnly(projectTask.getUuid(), anotherBranchTask.getUuid(), anotherProjectTask.getUuid()); assertThat(taskUuidsIn("ce_task_characteristics")).containsOnly(projectTask.getUuid(), anotherBranchTask.getUuid(), anotherProjectTask.getUuid(), "non existing task"); - underTest.deleteProject(dbSession, project.getProjectDto().getUuid(), project.getProjectDto().getQualifier(), project.getProjectDto().getName(), project.getProjectDto().getKey()); + underTest.deleteProject(dbSession, project.getProjectDto().getUuid(), project.getProjectDto().getQualifier(), project.getProjectDto().getName(), + project.getProjectDto().getKey()); dbSession.commit(); assertThat(uuidsIn("ce_activity")).containsOnly(anotherProjectTask.getUuid()); @@ -887,7 +894,8 @@ public class PurgeDaoIT { assertThat(uuidsIn("ce_activity")).containsOnly(projectTask.getUuid(), anotherBranchTask.getUuid(), anotherProjectTask.getUuid()); assertThat(taskUuidsIn("ce_task_message")).containsOnly(projectTask.getUuid(), anotherBranchTask.getUuid(), anotherProjectTask.getUuid(), "non existing task"); - underTest.deleteProject(dbSession, project.getProjectDto().getUuid(), project.getProjectDto().getQualifier(), project.getProjectDto().getName(), project.getProjectDto().getKey()); + underTest.deleteProject(dbSession, project.getProjectDto().getUuid(), project.getProjectDto().getQualifier(), project.getProjectDto().getName(), + project.getProjectDto().getKey()); dbSession.commit(); assertThat(uuidsIn("ce_activity")).containsOnly(anotherProjectTask.getUuid()); @@ -1602,7 +1610,8 @@ public class PurgeDaoIT { verifyNoEffect(componentDbTester.insertPrivateProject().getMainBranchComponent()); verifyNoEffect(componentDbTester.insertPublicProject().getMainBranchComponent()); verifyNoEffect(componentDbTester.insertPrivatePortfolio()); - verifyNoEffect(componentDbTester.insertPrivatePortfolio(), componentDbTester.insertPrivateProject().getMainBranchComponent(), componentDbTester.insertPublicProject().getMainBranchComponent()); + verifyNoEffect(componentDbTester.insertPrivatePortfolio(), componentDbTester.insertPrivateProject().getMainBranchComponent(), + componentDbTester.insertPublicProject().getMainBranchComponent()); } @Test @@ -1637,7 +1646,8 @@ public class PurgeDaoIT { @Test public void deleteNonRootComponents_deletes_only_non_root_components_of_a_project_from_table_components() { - ComponentDto project = new Random().nextBoolean() ? db.components().insertPublicProject().getMainBranchComponent() : db.components().insertPrivateProject().getMainBranchComponent(); + ComponentDto project = new Random().nextBoolean() ? db.components().insertPublicProject().getMainBranchComponent() + : db.components().insertPrivateProject().getMainBranchComponent(); ComponentDto dir1 = db.components().insertComponent(newDirectory(project, "A/B")); ComponentDto file1 = db.components().insertComponent(newFileDto(project, dir1)); @@ -1684,7 +1694,8 @@ public class PurgeDaoIT { @Test public void deleteNonRootComponents_deletes_only_specified_non_root_components_of_a_project_from_table_components() { - ComponentDto project = new Random().nextBoolean() ? db.components().insertPublicProject().getMainBranchComponent() : db.components().insertPrivateProject().getMainBranchComponent(); + ComponentDto project = new Random().nextBoolean() ? db.components().insertPublicProject().getMainBranchComponent() + : db.components().insertPrivateProject().getMainBranchComponent(); ComponentDto dir1 = db.components().insertComponent(newDirectory(project, "A/B")); ComponentDto dir2 = db.components().insertComponent(newDirectory(project, "A/C")); ComponentDto file1 = db.components().insertComponent(newFileDto(project, dir1)); @@ -1722,7 +1733,8 @@ public class PurgeDaoIT { underTest.deleteNonRootComponentsInView(dbSession, asList(subview1, pc2)); assertThat(uuidsIn("components")) - .containsOnly(view.uuid(), projects[0].getMainBranchComponent().uuid(), projects[1].getMainBranchComponent().uuid(), projects[2].getMainBranchComponent().uuid(), subview2.uuid(), pc1.uuid()); + .containsOnly(view.uuid(), projects[0].getMainBranchComponent().uuid(), projects[1].getMainBranchComponent().uuid(), projects[2].getMainBranchComponent().uuid(), + subview2.uuid(), pc1.uuid()); assertThat(uuidsIn("projects")).containsOnly(projects[0].getProjectDto().getUuid(), projects[1].getProjectDto().getUuid(), projects[2].getProjectDto().getUuid()); } @@ -1865,17 +1877,17 @@ public class PurgeDaoIT { private void insertPropertyFor(ComponentDto... components) { Stream.of(components).forEach(componentDto -> db.properties().insertProperty(new PropertyDto() - .setKey(randomAlphabetic(3)) - .setValue(randomAlphabetic(3)) - .setEntityUuid(componentDto.uuid()), + .setKey(randomAlphabetic(3)) + .setValue(randomAlphabetic(3)) + .setEntityUuid(componentDto.uuid()), componentDto.getKey(), componentDto.name(), componentDto.qualifier(), null)); } private void insertPropertyFor(Collection branches) { branches.stream().forEach(branchDto -> db.properties().insertProperty(new PropertyDto() - .setKey(randomAlphabetic(3)) - .setValue(randomAlphabetic(3)) - .setEntityUuid(branchDto.getUuid()), + .setKey(randomAlphabetic(3)) + .setValue(randomAlphabetic(3)) + .setEntityUuid(branchDto.getUuid()), null, branchDto.getKey(), null, null)); } @@ -2020,4 +2032,10 @@ public class PurgeDaoIT { addComponentsSnapshotsAndIssuesToBranch(componentDto, rule, branchAge); } + private int countPurgedSnapshots() { + Dialect dialect = db.getDbClient().getDatabase().getDialect(); + String bool = dialect.getTrueSqlValue(); + return db.countSql("select count(*) from snapshots where purged = " + bool); + } + } diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/component/SnapshotMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/component/SnapshotMapper.xml index 1d5fcc6abbd..4ebbc769fff 100644 --- a/server/sonar-db-dao/src/main/resources/org/sonar/db/component/SnapshotMapper.xml +++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/component/SnapshotMapper.xml @@ -215,7 +215,8 @@ period1_mode, period1_param, period1_date, - revision + revision, + purged ) values ( #{uuid, jdbcType=VARCHAR}, @@ -229,7 +230,8 @@ #{periodMode, jdbcType=VARCHAR}, #{periodParam, jdbcType=VARCHAR}, #{periodDate, jdbcType=BIGINT}, - #{revision, jdbcType=VARCHAR} + #{revision, jdbcType=VARCHAR}, + ${_false} ) diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/purge/PurgeMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/purge/PurgeMapper.xml index 79987cf193a..770955921ca 100644 --- a/server/sonar-db-dao/src/main/resources/org/sonar/db/purge/PurgeMapper.xml +++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/purge/PurgeMapper.xml @@ -14,7 +14,7 @@ and s.islast=#{islast} - and (s.purge_status is null or s.purge_status=0) + and s.purged = ${_false} and s.status in @@ -178,7 +178,7 @@ update snapshots set - purge_status = 1 + purged = ${_true} where uuid in diff --git a/server/sonar-db-dao/src/schema/schema-sq.ddl b/server/sonar-db-dao/src/schema/schema-sq.ddl index 4d9d6560ed1..46f7cdb381d 100644 --- a/server/sonar-db-dao/src/schema/schema-sq.ddl +++ b/server/sonar-db-dao/src/schema/schema-sq.ddl @@ -967,14 +967,14 @@ CREATE TABLE "SNAPSHOTS"( "STATUS" CHARACTER VARYING(4) DEFAULT 'U' NOT NULL, "ISLAST" BOOLEAN DEFAULT FALSE NOT NULL, "VERSION" CHARACTER VARYING(500), - "PURGE_STATUS" INTEGER, "BUILD_STRING" CHARACTER VARYING(100), "REVISION" CHARACTER VARYING(100), "BUILD_DATE" BIGINT, "PERIOD1_MODE" CHARACTER VARYING(100), "PERIOD1_PARAM" CHARACTER VARYING(100), "PERIOD1_DATE" BIGINT, - "CREATED_AT" BIGINT + "CREATED_AT" BIGINT, + "PURGED" BOOLEAN NOT NULL ); ALTER TABLE "SNAPSHOTS" ADD CONSTRAINT "PK_SNAPSHOTS" PRIMARY KEY("UUID"); CREATE INDEX "SNAPSHOTS_ROOT_COMPONENT_UUID" ON "SNAPSHOTS"("ROOT_COMPONENT_UUID" NULLS FIRST); diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v102/CreateBooleanPurgedColumnInSnapshots.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v102/CreateBooleanPurgedColumnInSnapshots.java new file mode 100644 index 00000000000..e2d2e48fee0 --- /dev/null +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v102/CreateBooleanPurgedColumnInSnapshots.java @@ -0,0 +1,54 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db.migration.version.v102; + +import java.sql.SQLException; +import org.sonar.db.Database; +import org.sonar.db.DatabaseUtils; +import org.sonar.server.platform.db.migration.def.BooleanColumnDef; +import org.sonar.server.platform.db.migration.sql.AddColumnsBuilder; +import org.sonar.server.platform.db.migration.step.DdlChange; + +public class CreateBooleanPurgedColumnInSnapshots extends DdlChange { + private static final String COLUMN_NAME = "purged"; + private static final String TABLE_NAME = "snapshots"; + + public CreateBooleanPurgedColumnInSnapshots(Database db) { + super(db); + } + + @Override + public void execute(Context context) throws SQLException { + if (checkIfColumnExists()) { + return; + } + BooleanColumnDef columnDef = BooleanColumnDef.newBooleanColumnDefBuilder(COLUMN_NAME).setIsNullable(true).build(); + context.execute(new AddColumnsBuilder(getDialect(), TABLE_NAME).addColumn(columnDef).build()); + } + + public boolean checkIfColumnExists() throws SQLException { + try (var connection = getDatabase().getDataSource().getConnection()) { + if (DatabaseUtils.tableColumnExists(connection, TABLE_NAME, COLUMN_NAME)) { + return true; + } + } + return false; + } +} diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v102/DbVersion102.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v102/DbVersion102.java index 39ccad83e79..cd6bfeb4d7e 100644 --- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v102/DbVersion102.java +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v102/DbVersion102.java @@ -70,6 +70,11 @@ public class DbVersion102 implements DbVersion { .add(10_2_018, "Drop index 'component_uuid' in 'snapshots' table", DropIndexComponentUuidInSnapshots.class) .add(10_2_019, "Rename 'component_uuid' in 'snapshots' table to 'root_component_uuid'", RenameComponentUuidInSnapshots.class) .add(10_2_020, "Create index 'snapshots_root_component_uuid' in 'snapshots' table", CreateIndexRootComponentUuidInSnapshots.class) + + .add(10_2_021, "Create 'purged' column in 'snapshots' table", CreateBooleanPurgedColumnInSnapshots.class) + .add(10_2_022, "Populate 'purged' column in 'snapshots' table", PopulatePurgedColumnInSnapshots.class) + .add(10_2_023, "Make 'purged' column not nullable in 'snapshots' table", MakePurgedColumnNotNullableInSnapshots.class) + .add(10_2_024, "Drop 'purge_status' column in 'snapshots' table", DropPurgeStatusColumnInSnapshots.class) ; } } diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v102/DropPurgeStatusColumnInSnapshots.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v102/DropPurgeStatusColumnInSnapshots.java new file mode 100644 index 00000000000..c8aa2abbb91 --- /dev/null +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v102/DropPurgeStatusColumnInSnapshots.java @@ -0,0 +1,32 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db.migration.version.v102; + +import org.sonar.db.Database; +import org.sonar.server.platform.db.migration.step.DropColumnChange; + +public class DropPurgeStatusColumnInSnapshots extends DropColumnChange { + private static final String COLUMN_NAME = "purge_status"; + private static final String TABLE_NAME = "snapshots"; + + public DropPurgeStatusColumnInSnapshots(Database db) { + super(db, TABLE_NAME, COLUMN_NAME); + } +} diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v102/MakePurgedColumnNotNullableInSnapshots.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v102/MakePurgedColumnNotNullableInSnapshots.java new file mode 100644 index 00000000000..677f2d4f264 --- /dev/null +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v102/MakePurgedColumnNotNullableInSnapshots.java @@ -0,0 +1,39 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db.migration.version.v102; + +import java.sql.SQLException; +import org.sonar.db.Database; +import org.sonar.server.platform.db.migration.def.BooleanColumnDef; +import org.sonar.server.platform.db.migration.sql.AlterColumnsBuilder; +import org.sonar.server.platform.db.migration.step.DdlChange; + +public class MakePurgedColumnNotNullableInSnapshots extends DdlChange { + private static final String COLUMN_NAME = "purged"; + private static final String TABLE_NAME = "snapshots"; + public MakePurgedColumnNotNullableInSnapshots(Database db) { + super(db); + } + @Override + public void execute(Context context) throws SQLException { + BooleanColumnDef columnDef = BooleanColumnDef.newBooleanColumnDefBuilder(COLUMN_NAME).setIsNullable(false).build(); + context.execute(new AlterColumnsBuilder(getDialect(), TABLE_NAME).updateColumn(columnDef).build()); + } +} diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v102/PopulatePurgedColumnInSnapshots.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v102/PopulatePurgedColumnInSnapshots.java new file mode 100644 index 00000000000..efdff45fc38 --- /dev/null +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v102/PopulatePurgedColumnInSnapshots.java @@ -0,0 +1,72 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db.migration.version.v102; + +import java.sql.SQLException; +import org.sonar.db.Database; +import org.sonar.db.DatabaseUtils; +import org.sonar.server.platform.db.migration.step.DataChange; +import org.sonar.server.platform.db.migration.step.MassUpdate; + +public class PopulatePurgedColumnInSnapshots extends DataChange { + private static final String SELECT_QUERY = """ + SELECT s.uuid, s.purge_status + FROM snapshots s + WHERE s.purged is null + """; + + private static final String UPDATE_QUERY = """ + UPDATE snapshots + SET purged=? + WHERE uuid=? + """; + + public PopulatePurgedColumnInSnapshots(Database db) { + super(db); + } + + @Override + protected void execute(Context context) throws SQLException { + if (!checkIfColumnExists()) { + return; + } + + MassUpdate massUpdate = context.prepareMassUpdate(); + massUpdate.select(SELECT_QUERY); + massUpdate.update(UPDATE_QUERY); + + massUpdate.execute((row, update, index) -> { + String snapshotUuid = row.getString(1); + Integer purgedStatus = row.getNullableInt(2); + update.setBoolean(1, purgedStatus != null && purgedStatus == 1) + .setString(2, snapshotUuid); + return true; + }); + } + + public boolean checkIfColumnExists() throws SQLException { + try (var connection = getDatabase().getDataSource().getConnection()) { + if (DatabaseUtils.tableColumnExists(connection, "snapshots", "purge_status")) { + return true; + } + } + return false; + } +} diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v102/CreateBooleanPurgedColumnInSnapshotsTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v102/CreateBooleanPurgedColumnInSnapshotsTest.java new file mode 100644 index 00000000000..952a6bd6909 --- /dev/null +++ b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v102/CreateBooleanPurgedColumnInSnapshotsTest.java @@ -0,0 +1,53 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db.migration.version.v102; + +import java.sql.SQLException; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.db.CoreDbTester; + +import static java.sql.Types.BOOLEAN; + +public class CreateBooleanPurgedColumnInSnapshotsTest { + + private static final String TABLE_NAME = "snapshots"; + private static final String COLUMN_NAME = "purged"; + + @Rule + public final CoreDbTester db = CoreDbTester.createForSchema(CreateBooleanPurgedColumnInSnapshotsTest.class, "schema.sql"); + + private final CreateBooleanPurgedColumnInSnapshots underTest = new CreateBooleanPurgedColumnInSnapshots(db.database()); + + @Test + public void execute_whenColumnDoesNotExist_shouldCreatePurgedColumn() throws SQLException { + db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME); + underTest.execute(); + db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, BOOLEAN, null, null); + } + + @Test + public void execute_whenExecutedTwice_shouldNotFail() throws SQLException { + db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME); + underTest.execute(); + underTest.execute(); + db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, BOOLEAN, null, null); + } +} diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v102/DropPurgeStatusColumnInSnapshotsTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v102/DropPurgeStatusColumnInSnapshotsTest.java new file mode 100644 index 00000000000..cceeff1809b --- /dev/null +++ b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v102/DropPurgeStatusColumnInSnapshotsTest.java @@ -0,0 +1,52 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db.migration.version.v102; + +import java.sql.SQLException; +import java.sql.Types; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.db.CoreDbTester; + +public class DropPurgeStatusColumnInSnapshotsTest { + + private static final String TABLE_NAME = "snapshots"; + private static final String COLUMN_NAME = "purge_status"; + + @Rule + public final CoreDbTester db = CoreDbTester.createForSchema(DropPurgeStatusColumnInSnapshotsTest.class, "schema.sql"); + + private final DropPurgeStatusColumnInSnapshots underTest = new DropPurgeStatusColumnInSnapshots(db.database()); + + @Test + public void drops_column() throws SQLException { + db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, Types.INTEGER, null, null); + underTest.execute(); + db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME); + } + + @Test + public void migration_is_reentrant() throws SQLException { + db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, Types.INTEGER, null, null); + underTest.execute(); + underTest.execute(); + db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME); + } +} diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v102/MakePurgedColumnNotNullableInSnapshotsTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v102/MakePurgedColumnNotNullableInSnapshotsTest.java new file mode 100644 index 00000000000..9e857e215bc --- /dev/null +++ b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v102/MakePurgedColumnNotNullableInSnapshotsTest.java @@ -0,0 +1,53 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db.migration.version.v102; + +import java.sql.SQLException; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.db.CoreDbTester; + +import static java.sql.Types.BOOLEAN; + +public class MakePurgedColumnNotNullableInSnapshotsTest { + private static final String TABLE_NAME = "snapshots"; + private static final String COLUMN_NAME = "purged"; + + @Rule + public final CoreDbTester db = CoreDbTester.createForSchema(MakePurgedColumnNotNullableInSnapshotsTest.class, "schema.sql"); + + private final MakePurgedColumnNotNullableInSnapshots underTest = new MakePurgedColumnNotNullableInSnapshots(db.database()); + + @Test + public void execute_whenColumnIsNullable_shouldMakeColumnNullable() throws SQLException { + db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, BOOLEAN, null, true); + underTest.execute(); + db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, BOOLEAN, null, false); + } + + @Test + public void execute_whenExecutedTwice_shouldMakeColumnNullable() throws SQLException { + db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, BOOLEAN, null, true); + underTest.execute(); + underTest.execute(); + db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, BOOLEAN, null, false); + } + +} diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v102/PopulatePurgedColumnInSnapshotsTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v102/PopulatePurgedColumnInSnapshotsTest.java new file mode 100644 index 00000000000..f4515138b13 --- /dev/null +++ b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v102/PopulatePurgedColumnInSnapshotsTest.java @@ -0,0 +1,73 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db.migration.version.v102; + +import java.sql.SQLException; +import javax.annotation.Nullable; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.db.CoreDbTester; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.tuple; + +public class PopulatePurgedColumnInSnapshotsTest { + private static final String TABLE_NAME = "snapshots"; + + @Rule + public final CoreDbTester db = CoreDbTester.createForSchema(PopulatePurgedColumnInSnapshotsTest.class, "schema.sql"); + + private final PopulatePurgedColumnInSnapshots underTest = new PopulatePurgedColumnInSnapshots(db.database()); + + @Test + public void execute_whenSnapshotsDoesNotExist_shouldNotFail() { + assertThatCode(underTest::execute) + .doesNotThrowAnyException(); + } + + @Test + public void execute_whenSnapshotsExist_shouldPopulatePurgedColumn() throws SQLException { + insertSnapshot("uuid-1", null); + insertSnapshot("uuid-2", 1); + insertSnapshot("uuid-3", 0); + insertSnapshot("uuid-4", null); + + underTest.execute(); + + assertThat(db.select("select UUID, PURGED from snapshots")) + .extracting(stringObjectMap -> stringObjectMap.get("UUID"), stringObjectMap -> stringObjectMap.get("PURGED")) + .containsExactlyInAnyOrder( + tuple("uuid-1", false), + tuple("uuid-2", true), + tuple("uuid-3", false), + tuple("uuid-4", false)); + } + + private void insertSnapshot(String uuid, @Nullable Integer status) { + db.executeInsert(TABLE_NAME, + "UUID", uuid, + "ROOT_COMPONENT_UUID", "", + "STATUS", "", + "ISLAST", true, + "PURGE_STATUS", status, + "PURGED", null); + } +} diff --git a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v102/CreateBooleanPurgedColumnInSnapshotsTest/schema.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v102/CreateBooleanPurgedColumnInSnapshotsTest/schema.sql new file mode 100644 index 00000000000..c11248e5729 --- /dev/null +++ b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v102/CreateBooleanPurgedColumnInSnapshotsTest/schema.sql @@ -0,0 +1,17 @@ +CREATE TABLE "SNAPSHOTS"( + "UUID" CHARACTER VARYING(50) NOT NULL, + "ROOT_COMPONENT_UUID" CHARACTER VARYING(50) NOT NULL, + "STATUS" CHARACTER VARYING(4) DEFAULT 'U' NOT NULL, + "ISLAST" BOOLEAN DEFAULT FALSE NOT NULL, + "VERSION" CHARACTER VARYING(500), + "PURGE_STATUS" INTEGER, + "BUILD_STRING" CHARACTER VARYING(100), + "REVISION" CHARACTER VARYING(100), + "BUILD_DATE" BIGINT, + "PERIOD1_MODE" CHARACTER VARYING(100), + "PERIOD1_PARAM" CHARACTER VARYING(100), + "PERIOD1_DATE" BIGINT, + "CREATED_AT" BIGINT +); +ALTER TABLE "SNAPSHOTS" ADD CONSTRAINT "PK_SNAPSHOTS" PRIMARY KEY("UUID"); +CREATE INDEX "SNAPSHOTS_ROOT_COMPONENT_UUID" ON "SNAPSHOTS"("ROOT_COMPONENT_UUID" NULLS FIRST); diff --git a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v102/DropPurgeStatusColumnInSnapshotsTest/schema.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v102/DropPurgeStatusColumnInSnapshotsTest/schema.sql new file mode 100644 index 00000000000..c5c483c982d --- /dev/null +++ b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v102/DropPurgeStatusColumnInSnapshotsTest/schema.sql @@ -0,0 +1,18 @@ +CREATE TABLE "SNAPSHOTS"( + "UUID" CHARACTER VARYING(50) NOT NULL, + "ROOT_COMPONENT_UUID" CHARACTER VARYING(50) NOT NULL, + "STATUS" CHARACTER VARYING(4) DEFAULT 'U' NOT NULL, + "ISLAST" BOOLEAN DEFAULT FALSE NOT NULL, + "VERSION" CHARACTER VARYING(500), + "PURGE_STATUS" INTEGER, + "BUILD_STRING" CHARACTER VARYING(100), + "REVISION" CHARACTER VARYING(100), + "BUILD_DATE" BIGINT, + "PERIOD1_MODE" CHARACTER VARYING(100), + "PERIOD1_PARAM" CHARACTER VARYING(100), + "PERIOD1_DATE" BIGINT, + "CREATED_AT" BIGINT, + "PURGED" BOOLEAN NOT NULL +); +ALTER TABLE "SNAPSHOTS" ADD CONSTRAINT "PK_SNAPSHOTS" PRIMARY KEY("UUID"); +CREATE INDEX "SNAPSHOTS_ROOT_COMPONENT_UUID" ON "SNAPSHOTS"("ROOT_COMPONENT_UUID" NULLS FIRST); diff --git a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v102/MakePurgedColumnNotNullableInSnapshotsTest/schema.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v102/MakePurgedColumnNotNullableInSnapshotsTest/schema.sql new file mode 100644 index 00000000000..354baa7eeab --- /dev/null +++ b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v102/MakePurgedColumnNotNullableInSnapshotsTest/schema.sql @@ -0,0 +1,18 @@ +CREATE TABLE "SNAPSHOTS"( + "UUID" CHARACTER VARYING(50) NOT NULL, + "ROOT_COMPONENT_UUID" CHARACTER VARYING(50) NOT NULL, + "STATUS" CHARACTER VARYING(4) DEFAULT 'U' NOT NULL, + "ISLAST" BOOLEAN DEFAULT FALSE NOT NULL, + "VERSION" CHARACTER VARYING(500), + "PURGE_STATUS" INTEGER, + "BUILD_STRING" CHARACTER VARYING(100), + "REVISION" CHARACTER VARYING(100), + "BUILD_DATE" BIGINT, + "PERIOD1_MODE" CHARACTER VARYING(100), + "PERIOD1_PARAM" CHARACTER VARYING(100), + "PERIOD1_DATE" BIGINT, + "CREATED_AT" BIGINT, + "PURGED" BOOLEAN +); +ALTER TABLE "SNAPSHOTS" ADD CONSTRAINT "PK_SNAPSHOTS" PRIMARY KEY("UUID"); +CREATE INDEX "SNAPSHOTS_ROOT_COMPONENT_UUID" ON "SNAPSHOTS"("ROOT_COMPONENT_UUID" NULLS FIRST); diff --git a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v102/PopulatePurgedColumnInSnapshotsTest/schema.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v102/PopulatePurgedColumnInSnapshotsTest/schema.sql new file mode 100644 index 00000000000..354baa7eeab --- /dev/null +++ b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v102/PopulatePurgedColumnInSnapshotsTest/schema.sql @@ -0,0 +1,18 @@ +CREATE TABLE "SNAPSHOTS"( + "UUID" CHARACTER VARYING(50) NOT NULL, + "ROOT_COMPONENT_UUID" CHARACTER VARYING(50) NOT NULL, + "STATUS" CHARACTER VARYING(4) DEFAULT 'U' NOT NULL, + "ISLAST" BOOLEAN DEFAULT FALSE NOT NULL, + "VERSION" CHARACTER VARYING(500), + "PURGE_STATUS" INTEGER, + "BUILD_STRING" CHARACTER VARYING(100), + "REVISION" CHARACTER VARYING(100), + "BUILD_DATE" BIGINT, + "PERIOD1_MODE" CHARACTER VARYING(100), + "PERIOD1_PARAM" CHARACTER VARYING(100), + "PERIOD1_DATE" BIGINT, + "CREATED_AT" BIGINT, + "PURGED" BOOLEAN +); +ALTER TABLE "SNAPSHOTS" ADD CONSTRAINT "PK_SNAPSHOTS" PRIMARY KEY("UUID"); +CREATE INDEX "SNAPSHOTS_ROOT_COMPONENT_UUID" ON "SNAPSHOTS"("ROOT_COMPONENT_UUID" NULLS FIRST);