]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9028 add PurgeDao.deleteNonRootComponents 2143/head
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Fri, 2 Jun 2017 16:40:07 +0000 (18:40 +0200)
committerSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Mon, 5 Jun 2017 14:24:55 +0000 (16:24 +0200)
server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeCommands.java
server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeDao.java
server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeMapper.java
server/sonar-db-dao/src/main/resources/org/sonar/db/purge/PurgeMapper.xml
server/sonar-db-dao/src/test/java/org/sonar/db/purge/PurgeDaoTest.java
server/sonar-server/src/main/java/org/sonar/server/component/ComponentCleanerService.java

index 7e6c0f1bf6fada1ff08fd9d7ea5ae3228c367426..87cccc8862377eafa1df39a56ddd007b0ea9015f 100644 (file)
@@ -86,7 +86,7 @@ class PurgeCommands {
   }
 
   @VisibleForTesting
-  protected void deleteAnalyses(List<IdUuidPair> analysisIdUuids) {
+  void deleteAnalyses(List<IdUuidPair> analysisIdUuids) {
     List<List<String>> analysisUuidsPartitions = Lists.partition(IdUuidPairs.uuids(analysisIdUuids), MAX_SNAPSHOTS_PER_QUERY);
 
     deleteAnalysisDuplications(analysisUuidsPartitions);
@@ -107,7 +107,7 @@ class PurgeCommands {
     profiler.stop();
   }
 
-  public void purgeAnalyses(List<IdUuidPair> analysisUuids) {
+  void purgeAnalyses(List<IdUuidPair> analysisUuids) {
     List<List<String>> analysisUuidsPartitions = Lists.partition(IdUuidPairs.uuids(analysisUuids), MAX_SNAPSHOTS_PER_QUERY);
 
     deleteAnalysisDuplications(analysisUuidsPartitions);
@@ -156,6 +156,24 @@ class PurgeCommands {
     profiler.stop();
   }
 
+  void deleteIssues(List<String> componentUuids) {
+    if (componentUuids.isEmpty()) {
+      return;
+    }
+
+    List<List<String>> uuidPartitions = Lists.partition(componentUuids, MAX_RESOURCES_PER_QUERY);
+
+    profiler.start("deleteIssues (issue_changes)");
+    uuidPartitions.forEach(purgeMapper::deleteIssueChangesByComponentUuids);
+    session.commit();
+    profiler.stop();
+
+    profiler.start("deleteIssues (issues)");
+    uuidPartitions.forEach(purgeMapper::deleteIssuesByComponentUuids);
+    session.commit();
+    profiler.stop();
+  }
+
   void deleteLinks(String rootUuid) {
     profiler.start("deleteLinks (project_links)");
     purgeMapper.deleteProjectLinksByComponentUuid(rootUuid);
@@ -164,6 +182,9 @@ class PurgeCommands {
   }
 
   void deleteByRootAndModulesOrSubviews(List<IdUuidPair> rootAndModulesOrSubviewsIds) {
+    if (rootAndModulesOrSubviewsIds.isEmpty()) {
+      return;
+    }
     List<List<Long>> idPartitions = Lists.partition(IdUuidPairs.ids(rootAndModulesOrSubviewsIds), MAX_RESOURCES_PER_QUERY);
     List<List<String>> uuidsPartitions = Lists.partition(IdUuidPairs.uuids(rootAndModulesOrSubviewsIds), MAX_RESOURCES_PER_QUERY);
 
@@ -185,7 +206,29 @@ class PurgeCommands {
     profiler.stop();
   }
 
-  public void deleteComponentMeasures(List<String> analysisUuids, List<String> componentUuids) {
+  void deleteComponents(List<String> componentUuids) {
+    if (componentUuids.isEmpty()) {
+      return;
+    }
+
+    profiler.start("deleteComponents (projects)");
+    Lists.partition(componentUuids, MAX_RESOURCES_PER_QUERY).forEach(purgeMapper::deleteComponentsByUuids);
+    session.commit();
+    profiler.stop();
+  }
+
+  void deleteComponentMeasures(List<String> componentUuids) {
+    if (componentUuids.isEmpty()) {
+      return;
+    }
+
+    profiler.start("deleteComponentMeasures (project_measures)");
+    Lists.partition(componentUuids, MAX_RESOURCES_PER_QUERY).forEach(purgeMapper::fullDeleteComponentMeasures);
+    session.commit();
+    profiler.stop();
+  }
+
+  void deleteComponentMeasures(List<String> analysisUuids, List<String> componentUuids) {
     if (analysisUuids.isEmpty() || componentUuids.isEmpty()) {
       return;
     }
@@ -203,28 +246,39 @@ class PurgeCommands {
     profiler.stop();
   }
 
-  public void deleteFileSources(String rootUuid) {
+  void deleteFileSources(List<String> componentUuids) {
+    if (componentUuids.isEmpty()) {
+      return;
+    }
+
+    profiler.start("deleteFileSources (file_sources)");
+    Lists.partition(componentUuids, MAX_RESOURCES_PER_QUERY).forEach(purgeMapper::deleteFileSourcesByFileUuid);
+    session.commit();
+    profiler.stop();
+  }
+
+  void deleteFileSources(String rootUuid) {
     profiler.start("deleteFileSources (file_sources)");
     purgeMapper.deleteFileSourcesByProjectUuid(rootUuid);
     session.commit();
     profiler.stop();
   }
 
-  public void deleteCeActivity(String rootUuid) {
+  void deleteCeActivity(String rootUuid) {
     profiler.start("deleteCeActivity (ce_activity)");
     purgeMapper.deleteCeActivityByProjectUuid(rootUuid);
     session.commit();
     profiler.stop();
   }
 
-  public void deleteCeQueue(String rootUuid) {
+  void deleteCeQueue(String rootUuid) {
     profiler.start("deleteCeQueue (ce_queue)");
     purgeMapper.deleteCeQueueByProjectUuid(rootUuid);
     session.commit();
     profiler.stop();
   }
 
-  public void deleteWebhookDeliveries(String rootUuid) {
+  void deleteWebhookDeliveries(String rootUuid) {
     profiler.start("deleteWebhookDeliveries (webhook_deliveries)");
     purgeMapper.deleteWebhookDeliveriesByProjectUuid(rootUuid);
     session.commit();
index 59b621b7747c9165aa97dc19c7005948825b0dbe..a82a5c96cfa35bdab9b9be47d9a2cca9d231d1d0 100644 (file)
  */
 package org.sonar.db.purge;
 
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
 import java.util.List;
+import java.util.Set;
 import org.sonar.api.utils.System2;
 import org.sonar.api.utils.log.Logger;
 import org.sonar.api.utils.log.Loggers;
@@ -45,6 +48,12 @@ import static org.sonar.db.DatabaseUtils.executeLargeInputs;
 public class PurgeDao implements Dao {
   private static final Logger LOG = Loggers.get(PurgeDao.class);
   private static final String[] UNPROCESSED_STATUS = new String[] {"U"};
+  private static final ImmutableSet<String> QUALIFIERS_PROJECT_VIEW = ImmutableSet.of("TRK", "VW");
+  private static final ImmutableSet<String> QUALIFIERS_MODULE_SUBVIEW = ImmutableSet.of("BRC", "SVW");
+  private static final String SCOPE_PROJECT = "PRJ";
+  private static final String SCOPE_FILE = "FIL";
+  private static final String QUALIFIER_FILE = "FIL";
+  private static final String QUALIFIER_UNIT_TEST = "UTS";
 
   private final ComponentDao componentDao;
   private final System2 system2;
@@ -145,14 +154,14 @@ public class PurgeDao implements Dao {
     return result;
   }
 
-  public PurgeDao deleteProject(DbSession session, String uuid) {
+  public PurgeDao deleteRootComponent(DbSession session, String uuid) {
     PurgeProfiler profiler = new PurgeProfiler();
     PurgeCommands purgeCommands = new PurgeCommands(session, profiler);
-    deleteProject(uuid, mapper(session), purgeCommands);
+    deleteRootComponent(uuid, mapper(session), purgeCommands);
     return this;
   }
 
-  private static void deleteProject(String rootUuid, PurgeMapper mapper, PurgeCommands commands) {
+  private static void deleteRootComponent(String rootUuid, PurgeMapper mapper, PurgeCommands commands) {
     List<IdUuidPair> rootAndModulesOrSubviews = mapper.selectRootAndModulesOrSubviewsByProjectUuid(rootUuid);
     long rootId = rootAndModulesOrSubviews.stream()
       .filter(pair -> pair.getUuid().equals(rootUuid))
@@ -171,6 +180,55 @@ public class PurgeDao implements Dao {
     commands.deleteWebhookDeliveries(rootUuid);
   }
 
+  /**
+   * Delete the non root components (ie. neither project nor view) from the specified collection of {@link ComponentDto}
+   * and data from their child tables.
+   * <p>
+   *   This method has no effect when passed an empty collection or only root components.
+   * </p>
+   */
+  public PurgeDao deleteNonRootComponents(DbSession dbSession, Collection<ComponentDto> components) {
+    Set<ComponentDto> nonRootComponents = components.stream().filter(PurgeDao::isNotRoot).collect(MoreCollectors.toSet());
+    if (nonRootComponents.isEmpty()) {
+      return this;
+    }
+
+    PurgeProfiler profiler = new PurgeProfiler();
+    PurgeCommands purgeCommands = new PurgeCommands(dbSession, profiler);
+    deleteNonRootComponents(nonRootComponents, purgeCommands);
+
+    return this;
+  }
+
+  private static void deleteNonRootComponents(Set<ComponentDto> nonRootComponents, PurgeCommands purgeCommands) {
+    List<IdUuidPair> modulesOrSubviews = nonRootComponents.stream()
+      .filter(PurgeDao::isModuleOrSubview)
+      .map(PurgeDao::toIdUuidPair)
+      .collect(MoreCollectors.toList());
+    purgeCommands.deleteByRootAndModulesOrSubviews(modulesOrSubviews);
+    List<String> nonRootComponentUuids = nonRootComponents.stream().map(ComponentDto::uuid).collect(MoreCollectors.toList(nonRootComponents.size()));
+    purgeCommands.deleteComponentMeasures(nonRootComponentUuids);
+    purgeCommands.deleteFileSources(nonRootComponents.stream().filter(PurgeDao::isFile).map(ComponentDto::uuid).collect(MoreCollectors.toList()));
+    purgeCommands.deleteIssues(nonRootComponentUuids);
+    purgeCommands.deleteComponents(nonRootComponentUuids);
+  }
+
+  private static boolean isNotRoot(ComponentDto dto) {
+    return !(SCOPE_PROJECT.equals(dto.scope()) && QUALIFIERS_PROJECT_VIEW.contains(dto.qualifier()));
+  }
+
+  private static boolean isModuleOrSubview(ComponentDto dto) {
+    return SCOPE_PROJECT.equals(dto.scope()) && QUALIFIERS_MODULE_SUBVIEW.contains(dto.qualifier());
+  }
+
+  private static boolean isFile(ComponentDto dto) {
+    return SCOPE_FILE.equals(dto.qualifier()) && ImmutableSet.of(QUALIFIER_FILE, QUALIFIER_UNIT_TEST).contains(dto.qualifier());
+  }
+
+  private static IdUuidPair toIdUuidPair(ComponentDto dto) {
+    return new IdUuidPair(dto.getId(), dto.uuid());
+  }
+
   public void deleteAnalyses(DbSession session, PurgeProfiler profiler, List<IdUuidPair> analysisIdUuids) {
     new PurgeCommands(session, profiler).deleteAnalyses(analysisIdUuids);
   }
index 6ea23a7994b774d79f6e264e6d641c6148c40b8b..ec9b49e737c943fea69bdef7ed10684c51310bc2 100644 (file)
@@ -40,6 +40,8 @@ public interface PurgeMapper {
 
   void deleteAnalysisMeasures(@Param("analysisUuids") List<String> analysisUuids);
 
+  void fullDeleteComponentMeasures(@Param("componentUuids") List<String> componentUuids);
+
   void deleteComponentMeasures(@Param("analysisUuids") List<String> analysisUuids, @Param("componentUuids") List<String> componentUuids);
 
   List<Long> selectMetricIdsWithoutHistoricalData();
@@ -56,6 +58,8 @@ public interface PurgeMapper {
 
   void deleteComponentsByProjectUuid(@Param("rootUuid") String rootUuid);
 
+  void deleteComponentsByUuids(@Param("componentUuids") List<String> componentUuids);
+
   void deleteGroupRolesByComponentId(@Param("rootId") long rootId);
 
   void deleteUserRolesByComponentId(@Param("rootId") long rootId);
@@ -72,6 +76,10 @@ public interface PurgeMapper {
 
   void deleteIssuesByProjectUuid(@Param("projectUuid") String projectUuid);
 
+  void deleteIssueChangesByComponentUuids(@Param("componentUuids") List<String> componentUuids);
+
+  void deleteIssuesByComponentUuids(@Param("componentUuids") List<String> componentUuids);
+
   List<String> selectOldClosedIssueKeys(@Param("projectUuid") String projectUuid, @Nullable @Param("toDate") Long toDate);
 
   void deleteIssuesFromKeys(@Param("keys") List<String> keys);
index 80f7f5787845e61e086e254f14c9b756b9dc29e8..dee0337d1ada5db945b2e1170def4b95c82d44c5 100644 (file)
       </foreach>
   </delete>
 
+  <delete id="fullDeleteComponentMeasures" parameterType="map">
+    delete from project_measures
+    where
+      component_uuid in
+      <foreach collection="componentUuids" open="(" close=")" item="componentUuid" separator=",">
+        #{componentUuid,jdbcType=VARCHAR}
+      </foreach>
+  </delete>
+
   <delete id="deleteComponentMeasures" parameterType="map">
     delete from project_measures
     where
       project_uuid = #{rootUuid,jdbcType=VARCHAR}
   </delete>
 
+  <delete id="deleteComponentsByUuids" parameterType="map">
+    delete from projects
+    where
+      uuid in
+        <foreach collection="componentUuids" open="(" close=")" item="componentUuid" separator=",">
+          #{componentUuid,jdbcType=VARCHAR}
+        </foreach>
+  </delete>
+
   <delete id="deleteGroupRolesByComponentId" parameterType="map">
     delete from group_roles
     where
     where project_uuid = #{projectUuid,jdbcType=VARCHAR}
   </delete>
 
+  <delete id="deleteIssueChangesByComponentUuids" parameterType="map">
+    delete from issue_changes ic
+    where
+      exists (
+        select
+          1
+        from issues i
+        where
+          i.kee=ic.issue_key
+          and i.component_uuid in
+            <foreach collection="componentUuids" open="(" close=")" item="componentUuid" separator=",">
+              #{componentUuid,jdbcType=VARCHAR}
+            </foreach>
+    )
+  </delete>
+
+  <!-- Mssql -->
+  <delete id="deleteIssueChangesByComponentUuids" databaseId="mssql" parameterType="map">
+    delete issue_changes from issue_changes
+    inner join issues on
+      issue_changes.issue_key=issues.kee
+    where
+      issues.project_uuid  in
+        <foreach collection="componentUuids" open="(" close=")" item="componentUuid" separator=",">
+          #{componentUuid,jdbcType=VARCHAR}
+        </foreach>
+  </delete>
+
+  <!-- Mysql -->
+  <delete id="deleteIssueChangesByComponentUuids" databaseId="mysql" parameterType="map">
+    delete ic from issue_changes as ic, issues as i
+    where
+      ic.issue_key=i.kee
+      and i.component_uuid  in
+        <foreach collection="componentUuids" open="(" close=")" item="componentUuid" separator=",">
+          #{componentUuid,jdbcType=VARCHAR}
+        </foreach>
+  </delete>
+
+  <delete id="deleteIssuesByComponentUuids" parameterType="map">
+    delete from issues
+    where
+      project_uuid  in
+        <foreach collection="componentUuids" open="(" close=")" item="componentUuid" separator=",">
+          #{componentUuid,jdbcType=VARCHAR}
+        </foreach>
+  </delete>
+
   <delete id="deleteFileSourcesByProjectUuid">
     delete from file_sources where project_uuid=#{rootProjectUuid,jdbcType=VARCHAR}
   </delete>
index 4f6052a2ed6fd5f55dcd262de14a8e6ae42c4839..2e196ac87a9875d86c4e02bdc51c21fcf02988a8 100644 (file)
@@ -24,6 +24,9 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Random;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import org.apache.commons.lang.math.RandomUtils;
 import org.junit.Rule;
 import org.junit.Test;
@@ -38,14 +41,28 @@ import org.sonar.db.DbTester;
 import org.sonar.db.ce.CeActivityDto;
 import org.sonar.db.ce.CeQueueDto;
 import org.sonar.db.ce.CeQueueDto.Status;
+import org.sonar.db.component.ComponentDbTester;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.db.component.ComponentTesting;
-
+import org.sonar.db.issue.IssueDto;
+import org.sonar.db.measure.MeasureDto;
+import org.sonar.db.measure.custom.CustomMeasureDto;
+import org.sonar.db.property.PropertyDto;
+import org.sonar.db.rule.RuleTesting;
+import org.sonar.db.source.FileSourceDto;
+
+import static java.util.Arrays.asList;
+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.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 import static org.sonar.db.ce.CeTaskTypes.REPORT;
+import static org.sonar.db.component.ComponentTesting.newDirectory;
+import static org.sonar.db.component.ComponentTesting.newFileDto;
+import static org.sonar.db.component.ComponentTesting.newProjectCopy;
 import static org.sonar.db.webhook.WebhookDbTesting.newWebhookDeliveryDto;
 import static org.sonar.db.webhook.WebhookDbTesting.selectAllDeliveryUuids;
 
@@ -132,7 +149,7 @@ public class PurgeDaoTest {
   public void delete_project_and_associated_data() {
     dbTester.prepareDbUnit(getClass(), "shouldDeleteProject.xml");
 
-    underTest.deleteProject(dbSession, "A");
+    underTest.deleteRootComponent(dbSession, "A");
     dbSession.commit();
 
     assertThat(dbTester.countRowsOfTable("projects")).isZero();
@@ -153,7 +170,7 @@ public class PurgeDaoTest {
     insertCeActivity(anotherLivingProject);
     dbSession.commit();
 
-    underTest.deleteProject(dbSession, projectToBeDeleted.uuid());
+    underTest.deleteRootComponent(dbSession, projectToBeDeleted.uuid());
     dbSession.commit();
 
     assertThat(dbTester.countRowsOfTable("ce_activity")).isEqualTo(1);
@@ -171,7 +188,7 @@ public class PurgeDaoTest {
     dbClient.ceQueueDao().insert(dbSession, createCeQueue(anotherLivingProject, Status.PENDING));
     dbSession.commit();
 
-    underTest.deleteProject(dbSession, projectToBeDeleted.uuid());
+    underTest.deleteRootComponent(dbSession, projectToBeDeleted.uuid());
     dbSession.commit();
 
     assertThat(dbTester.countRowsOfTable("ce_queue")).isEqualTo(1);
@@ -182,7 +199,7 @@ public class PurgeDaoTest {
   public void delete_view_and_child() {
     dbTester.prepareDbUnit(getClass(), "view_sub_view_and_tech_project.xml");
 
-    underTest.deleteProject(dbSession, "A");
+    underTest.deleteRootComponent(dbSession, "A");
     dbSession.commit();
     assertThat(dbTester.countSql("select count(1) from projects where uuid='A'")).isZero();
     assertThat(dbTester.countRowsOfTable("projects")).isZero();
@@ -195,30 +212,30 @@ public class PurgeDaoTest {
 
     expectedException.expect(IllegalArgumentException.class);
     expectedException.expectMessage("Couldn't find root component with uuid " + module.uuid());
-    
-    underTest.deleteProject(dbSession, module.uuid());
+
+    underTest.deleteRootComponent(dbSession, module.uuid());
   }
 
   @Test
   public void deleteProject_fails_with_IAE_if_specified_component_is_directory() {
     ComponentDto privateProject = dbTester.components().insertPrivateProject();
-    ComponentDto directory = dbTester.components().insertComponent(ComponentTesting.newDirectory(privateProject, "A/B"));
+    ComponentDto directory = dbTester.components().insertComponent(newDirectory(privateProject, "A/B"));
 
     expectedException.expect(IllegalArgumentException.class);
     expectedException.expectMessage("Couldn't find root component with uuid " + directory.uuid());
 
-    underTest.deleteProject(dbSession, directory.uuid());
+    underTest.deleteRootComponent(dbSession, directory.uuid());
   }
 
   @Test
   public void deleteProject_fails_with_IAE_if_specified_component_is_file() {
     ComponentDto privateProject = dbTester.components().insertPrivateProject();
-    ComponentDto file = dbTester.components().insertComponent(ComponentTesting.newFileDto(privateProject));
+    ComponentDto file = dbTester.components().insertComponent(newFileDto(privateProject));
 
     expectedException.expect(IllegalArgumentException.class);
     expectedException.expectMessage("Couldn't find root component with uuid " + file.uuid());
 
-    underTest.deleteProject(dbSession, file.uuid());
+    underTest.deleteRootComponent(dbSession, file.uuid());
   }
 
   @Test
@@ -229,7 +246,7 @@ public class PurgeDaoTest {
     expectedException.expect(IllegalArgumentException.class);
     expectedException.expectMessage("Couldn't find root component with uuid " + subview.uuid());
 
-    underTest.deleteProject(dbSession, subview.uuid());
+    underTest.deleteRootComponent(dbSession, subview.uuid());
   }
 
   @Test
@@ -237,7 +254,7 @@ public class PurgeDaoTest {
     dbTester.prepareDbUnit(getClass(), "view_sub_view_and_tech_project.xml");
 
     // view
-    underTest.deleteProject(dbSession, "A");
+    underTest.deleteRootComponent(dbSession, "A");
     dbSession.commit();
     assertThat(dbTester.countSql("select count(1) from projects where uuid='A'")).isZero();
   }
@@ -276,11 +293,390 @@ public class PurgeDaoTest {
     dbClient.webhookDeliveryDao().insert(dbSession, newWebhookDeliveryDto().setComponentUuid(project.uuid()).setUuid("D1"));
     dbClient.webhookDeliveryDao().insert(dbSession, newWebhookDeliveryDto().setComponentUuid("P2").setUuid("D2"));
 
-    underTest.deleteProject(dbSession, project.uuid());
+    underTest.deleteRootComponent(dbSession, project.uuid());
 
     assertThat(selectAllDeliveryUuids(dbTester, dbSession)).containsOnly("D2");
   }
 
+  @Test
+  public void deleteNonRootComponents_has_no_effect_when_parameter_is_empty() {
+    DbSession dbSession = mock(DbSession.class);
+
+    underTest.deleteNonRootComponents(dbSession, Collections.emptyList());
+
+    verifyZeroInteractions(dbSession);
+  }
+
+  @Test
+  public void deleteNonRootComponents_has_no_effect_when_parameter_contains_only_projects_and_or_views() {
+    ComponentDbTester componentDbTester = dbTester.components();
+
+    verifyNoEffect(componentDbTester.insertPrivateProject());
+    verifyNoEffect(componentDbTester.insertPublicProject());
+    verifyNoEffect(componentDbTester.insertView());
+    verifyNoEffect(componentDbTester.insertView(), componentDbTester.insertPrivateProject(), componentDbTester.insertPublicProject());
+  }
+
+  private void verifyNoEffect(ComponentDto firstRoot, ComponentDto... otherRoots) {
+    DbSession dbSession = mock(DbSession.class);
+
+    List<ComponentDto> componentDtos = Stream.concat(Stream.of(firstRoot), Arrays.stream(otherRoots)).collect(Collectors.toList());
+    Collections.shuffle(componentDtos); // order of collection must not matter
+    underTest.deleteNonRootComponents(dbSession, componentDtos);
+
+    verifyZeroInteractions(dbSession);
+  }
+
+  @Test
+  public void deleteNonRootComponents_deletes_only_non_root_components_of_a_project_from_table_PROJECTS() {
+    ComponentDto project = new Random().nextBoolean() ? dbTester.components().insertPublicProject() : dbTester.components().insertPrivateProject();
+    ComponentDto module1 = dbTester.components().insertComponent(ComponentTesting.newModuleDto(project));
+    ComponentDto module2 = dbTester.components().insertComponent(ComponentTesting.newModuleDto(module1));
+    ComponentDto dir1 = dbTester.components().insertComponent(newDirectory(module1, "A/B"));
+
+    List<ComponentDto> components = asList(
+      project,
+      module1,
+      module2,
+      dir1,
+      dbTester.components().insertComponent(newDirectory(module2, "A/C")),
+      dbTester.components().insertComponent(newFileDto(dir1)),
+      dbTester.components().insertComponent(newFileDto(module2)),
+      dbTester.components().insertComponent(newFileDto(project)));
+    Collections.shuffle(components);
+
+    underTest.deleteNonRootComponents(dbSession, components);
+
+    assertThat(getUuidsInTableProjects())
+      .containsOnly(project.uuid());
+  }
+
+  @Test
+  public void deleteNonRootComponents_deletes_only_non_root_components_of_a_view_from_table_PROJECTS() {
+    ComponentDto[] projects = {
+      dbTester.components().insertPrivateProject(),
+      dbTester.components().insertPrivateProject(),
+      dbTester.components().insertPrivateProject()
+    };
+
+    ComponentDto view = dbTester.components().insertView();
+    ComponentDto subview1 = dbTester.components().insertComponent(ComponentTesting.newSubView(view));
+    ComponentDto subview2 = dbTester.components().insertComponent(ComponentTesting.newSubView(subview1));
+    List<ComponentDto> components = asList(
+      view,
+      subview1,
+      subview2,
+      dbTester.components().insertComponent(newProjectCopy("a", projects[0], view)),
+      dbTester.components().insertComponent(newProjectCopy("b", projects[1], subview1)),
+      dbTester.components().insertComponent(newProjectCopy("c", projects[2], subview2)));
+    Collections.shuffle(components);
+
+    underTest.deleteNonRootComponents(dbSession, components);
+
+    assertThat(getUuidsInTableProjects())
+      .containsOnly(view.uuid(), projects[0].uuid(), projects[1].uuid(), projects[2].uuid());
+  }
+
+  private Stream<String> getUuidsInTableProjects() {
+    return dbTester.select("select uuid as \"UUID\" from projects").stream().map(row -> (String) row.get("UUID"));
+  }
+
+  @Test
+  public void deleteNonRootComponents_deletes_only_specified_non_root_components_of_a_project_from_table_PROJECTS() {
+    ComponentDto project = new Random().nextBoolean() ? dbTester.components().insertPublicProject() : dbTester.components().insertPrivateProject();
+    ComponentDto module1 = dbTester.components().insertComponent(ComponentTesting.newModuleDto(project));
+    ComponentDto module2 = dbTester.components().insertComponent(ComponentTesting.newModuleDto(module1));
+    ComponentDto dir1 = dbTester.components().insertComponent(newDirectory(module1, "A/B"));
+    ComponentDto dir2 = dbTester.components().insertComponent(newDirectory(module2, "A/C"));
+    ComponentDto file1 = dbTester.components().insertComponent(newFileDto(dir1));
+    ComponentDto file2 = dbTester.components().insertComponent(newFileDto(module2));
+    ComponentDto file3 = dbTester.components().insertComponent(newFileDto(project));
+
+    underTest.deleteNonRootComponents(dbSession, singletonList(file3));
+    assertThat(getUuidsInTableProjects())
+      .containsOnly(project.uuid(), module1.uuid(), module2.uuid(), dir1.uuid(), dir2.uuid(), file1.uuid(), file2.uuid());
+
+    underTest.deleteNonRootComponents(dbSession, asList(module1, dir2, file1));
+    assertThat(getUuidsInTableProjects())
+      .containsOnly(project.uuid(), module2.uuid(), dir1.uuid(), file2.uuid());
+  }
+
+  @Test
+  public void deleteNonRootComponents_deletes_only_specified_non_root_components_of_a_view_from_table_PROJECTS() {
+    ComponentDto[] projects = {
+      dbTester.components().insertPrivateProject(),
+      dbTester.components().insertPrivateProject(),
+      dbTester.components().insertPrivateProject()
+    };
+
+    ComponentDto view = dbTester.components().insertView();
+    ComponentDto subview1 = dbTester.components().insertComponent(ComponentTesting.newSubView(view));
+    ComponentDto subview2 = dbTester.components().insertComponent(ComponentTesting.newSubView(subview1));
+    ComponentDto pc1 = dbTester.components().insertComponent(newProjectCopy("a", projects[0], view));
+    ComponentDto pc2 = dbTester.components().insertComponent(newProjectCopy("b", projects[1], subview1));
+    ComponentDto pc3 = dbTester.components().insertComponent(newProjectCopy("c", projects[2], subview2));
+
+    underTest.deleteNonRootComponents(dbSession, singletonList(pc3));
+    assertThat(getUuidsInTableProjects())
+      .containsOnly(view.uuid(), projects[0].uuid(), projects[1].uuid(), projects[2].uuid(),
+        subview1.uuid(), subview2.uuid(), pc1.uuid(), pc2.uuid());
+
+    underTest.deleteNonRootComponents(dbSession, asList(subview1, pc2));
+    assertThat(getUuidsInTableProjects())
+      .containsOnly(view.uuid(), projects[0].uuid(), projects[1].uuid(), projects[2].uuid(), subview2.uuid(), pc1.uuid());
+  }
+
+  @Test
+  public void deleteNonRootComponents_deletes_measures_of_any_non_root_component_of_a_project() {
+    ComponentDto project = new Random().nextBoolean() ? dbTester.components().insertPublicProject() : dbTester.components().insertPrivateProject();
+    ComponentDto module1 = dbTester.components().insertComponent(ComponentTesting.newModuleDto(project));
+    ComponentDto dir1 = dbTester.components().insertComponent(newDirectory(module1, "A/B"));
+    ComponentDto file1 = dbTester.components().insertComponent(newFileDto(dir1));
+    insertIssueAndChangesFor(project, module1, dir1, file1);
+    assertThat(getComponentUuidsOfIssueChanges()).containsOnly(project.uuid(), module1.uuid(), dir1.uuid(), file1.uuid());
+
+    underTest.deleteNonRootComponents(dbSession, singletonList(file1));
+    assertThat(getComponentUuidsOfIssueChanges())
+      .containsOnly(project.uuid(), module1.uuid(), dir1.uuid());
+
+    underTest.deleteNonRootComponents(dbSession, asList(module1, dir1));
+    assertThat(getComponentUuidsOfIssueChanges())
+      .containsOnly(project.uuid());
+  }
+
+  @Test
+  public void deleteNonRootComponents_deletes_measures_of_any_non_root_component_of_a_view() {
+    ComponentDto view = dbTester.components().insertView();
+    ComponentDto subview = dbTester.components().insertComponent(ComponentTesting.newSubView(view));
+    ComponentDto pc = dbTester.components().insertComponent(newProjectCopy("a", dbTester.components().insertPrivateProject(), view));
+    insertMeasureFor(view, subview, pc);
+    assertThat(getComponentUuidsOfMeasures()).containsOnly(view.uuid(), subview.uuid(), pc.uuid());
+
+    underTest.deleteNonRootComponents(dbSession, singletonList(pc));
+    assertThat(getComponentUuidsOfMeasures())
+      .containsOnly(view.uuid(), subview.uuid());
+
+    underTest.deleteNonRootComponents(dbSession, asList(subview));
+    assertThat(getComponentUuidsOfMeasures())
+      .containsOnly(view.uuid());
+  }
+
+  @Test
+  public void deleteNonRootComponents_deletes_issues_and_changes_of_any_non_root_component_of_a_project() {
+    ComponentDto project = new Random().nextBoolean() ? dbTester.components().insertPublicProject() : dbTester.components().insertPrivateProject();
+    ComponentDto module1 = dbTester.components().insertComponent(ComponentTesting.newModuleDto(project));
+    ComponentDto dir1 = dbTester.components().insertComponent(newDirectory(module1, "A/B"));
+    ComponentDto file1 = dbTester.components().insertComponent(newFileDto(dir1));
+    insertIssueAndChangesFor(project, module1, dir1, file1);
+    assertThat(getComponentUuidsOfIssueChanges()).containsOnly(project.uuid(), module1.uuid(), dir1.uuid(), file1.uuid());
+
+    underTest.deleteNonRootComponents(dbSession, singletonList(file1));
+    assertThat(getComponentUuidsOfIssueChanges())
+      .containsOnly(project.uuid(), module1.uuid(), dir1.uuid());
+
+    underTest.deleteNonRootComponents(dbSession, asList(module1, dir1));
+    assertThat(getComponentUuidsOfIssueChanges())
+      .containsOnly(project.uuid());
+  }
+
+  @Test
+  public void deleteNonRootComponents_deletes_issues_and_changes_of_any_non_root_component_of_a_view() {
+    ComponentDto view = dbTester.components().insertView();
+    ComponentDto subview = dbTester.components().insertComponent(ComponentTesting.newSubView(view));
+    ComponentDto pc = dbTester.components().insertComponent(newProjectCopy("a", dbTester.components().insertPrivateProject(), view));
+    insertIssueAndChangesFor(view, subview, pc);
+    assertThat(getComponentUuidsOfIssueChanges()).containsOnly(view.uuid(), subview.uuid(), pc.uuid());
+
+    underTest.deleteNonRootComponents(dbSession, singletonList(pc));
+    assertThat(getComponentUuidsOfIssueChanges())
+      .containsOnly(view.uuid(), subview.uuid());
+
+    underTest.deleteNonRootComponents(dbSession, asList(subview));
+    assertThat(getComponentUuidsOfIssueChanges())
+      .containsOnly(view.uuid());
+  }
+
+  @Test
+  public void deleteNonRootComponents_deletes_file_sources_of_file_component_of_a_project_only() {
+    ComponentDto project = new Random().nextBoolean() ? dbTester.components().insertPublicProject() : dbTester.components().insertPrivateProject();
+    ComponentDto module1 = dbTester.components().insertComponent(ComponentTesting.newModuleDto(project));
+    ComponentDto dir1 = dbTester.components().insertComponent(newDirectory(module1, "A/B"));
+    ComponentDto file1 = dbTester.components().insertComponent(newFileDto(dir1));
+    ComponentDto file2 = dbTester.components().insertComponent(newFileDto(dir1));
+    insertFileSources(project, module1, dir1, file1, file2);
+    assertThat(getComponentUuidsOfFileSources()).containsOnly(project.uuid(), module1.uuid(), dir1.uuid(), file1.uuid(), file2.uuid());
+
+    underTest.deleteNonRootComponents(dbSession, singletonList(file1));
+    assertThat(getComponentUuidsOfFileSources())
+      .containsOnly(project.uuid(), module1.uuid(), dir1.uuid(), file2.uuid());
+
+    underTest.deleteNonRootComponents(dbSession, asList(module1, dir1, file2));
+    assertThat(getComponentUuidsOfFileSources())
+      .containsOnly(project.uuid(), module1.uuid(), dir1.uuid());
+  }
+
+  @Test
+  public void deleteNonRootComponents_does_not_delete_file_sources_of_non_root_components_of_a_view() {
+    ComponentDto view = dbTester.components().insertView();
+    ComponentDto subview = dbTester.components().insertComponent(ComponentTesting.newSubView(view));
+    ComponentDto pc1 = dbTester.components().insertComponent(newProjectCopy("a", dbTester.components().insertPrivateProject(), view));
+    ComponentDto pc2 = dbTester.components().insertComponent(newProjectCopy("b", dbTester.components().insertPrivateProject(), view));
+    insertFileSources(view, subview, pc1, pc2);
+    assertThat(getComponentUuidsOfFileSources()).containsOnly(view.uuid(), subview.uuid(), pc1.uuid(), pc2.uuid());
+
+    underTest.deleteNonRootComponents(dbSession, singletonList(pc1));
+    assertThat(getComponentUuidsOfFileSources())
+      .containsOnly(view.uuid(), subview.uuid(), pc1.uuid(), pc2.uuid());
+
+    underTest.deleteNonRootComponents(dbSession, asList(subview, pc2));
+    assertThat(getComponentUuidsOfFileSources())
+      .containsOnly(view.uuid(), subview.uuid(), pc1.uuid(), pc2.uuid());
+  }
+
+  @Test
+  public void deleteNonRootComponents_deletes_properties_of_modules_of_a_project() {
+    ComponentDto project = new Random().nextBoolean() ? dbTester.components().insertPublicProject() : dbTester.components().insertPrivateProject();
+    ComponentDto module1 = dbTester.components().insertComponent(ComponentTesting.newModuleDto(project));
+    ComponentDto module2 = dbTester.components().insertComponent(ComponentTesting.newModuleDto(module1));
+    ComponentDto module3 = dbTester.components().insertComponent(ComponentTesting.newModuleDto(project));
+    ComponentDto dir1 = dbTester.components().insertComponent(newDirectory(module1, "A/B"));
+    ComponentDto file1 = dbTester.components().insertComponent(newFileDto(dir1));
+    insertPropertyFor(project, module1, module2, module3, dir1, file1);
+    assertThat(getResourceIdOfProperties()).containsOnly(project.getId(), module1.getId(), module2.getId(), module3.getId(), dir1.getId(), file1.getId());
+
+    underTest.deleteNonRootComponents(dbSession, singletonList(module3));
+    assertThat(getResourceIdOfProperties())
+      .containsOnly(project.getId(), module1.getId(), module2.getId(), dir1.getId(), file1.getId());
+
+    underTest.deleteNonRootComponents(dbSession, asList(module1, module2, dir1, file1));
+    assertThat(getResourceIdOfProperties())
+      .containsOnly(project.getId(), dir1.getId(), file1.getId());
+  }
+
+  @Test
+  public void deleteNonRootComponents_properties_of_subviews_of_a_view() {
+    ComponentDto view = dbTester.components().insertView();
+    ComponentDto subview1 = dbTester.components().insertComponent(ComponentTesting.newSubView(view));
+    ComponentDto subview2 = dbTester.components().insertComponent(ComponentTesting.newSubView(subview1));
+    ComponentDto subview3 = dbTester.components().insertComponent(ComponentTesting.newSubView(view));
+    ComponentDto pc = dbTester.components().insertComponent(newProjectCopy("a", dbTester.components().insertPrivateProject(), view));
+    insertPropertyFor(view, subview1, subview2, subview3, pc);
+    assertThat(getResourceIdOfProperties()).containsOnly(view.getId(), subview1.getId(), subview2.getId(), subview3.getId(), pc.getId());
+
+    underTest.deleteNonRootComponents(dbSession, singletonList(subview1));
+    assertThat(getResourceIdOfProperties())
+      .containsOnly(view.getId(), subview2.getId(), subview3.getId(), pc.getId());
+
+    underTest.deleteNonRootComponents(dbSession, asList(subview2, subview3, pc));
+    assertThat(getResourceIdOfProperties())
+      .containsOnly(view.getId(), pc.getId());
+  }
+
+  @Test
+  public void deleteNonRootComponents_deletes_manual_measures_of_modules_of_a_project() {
+    ComponentDto project = new Random().nextBoolean() ? dbTester.components().insertPublicProject() : dbTester.components().insertPrivateProject();
+    ComponentDto module1 = dbTester.components().insertComponent(ComponentTesting.newModuleDto(project));
+    ComponentDto module2 = dbTester.components().insertComponent(ComponentTesting.newModuleDto(module1));
+    ComponentDto module3 = dbTester.components().insertComponent(ComponentTesting.newModuleDto(project));
+    ComponentDto dir1 = dbTester.components().insertComponent(newDirectory(module1, "A/B"));
+    ComponentDto file1 = dbTester.components().insertComponent(newFileDto(dir1));
+    insertManualMeasureFor(project, module1, module2, module3, dir1, file1);
+    assertThat(getComponentUuidsOfManualMeasures()).containsOnly(project.uuid(), module1.uuid(), module2.uuid(), module3.uuid(), dir1.uuid(), file1.uuid());
+
+    underTest.deleteNonRootComponents(dbSession, singletonList(module3));
+    assertThat(getComponentUuidsOfManualMeasures())
+      .containsOnly(project.uuid(), module1.uuid(), module2.uuid(), dir1.uuid(), file1.uuid());
+
+    underTest.deleteNonRootComponents(dbSession, asList(module1, module2, dir1, file1));
+    assertThat(getComponentUuidsOfManualMeasures())
+      .containsOnly(project.uuid(), dir1.uuid(), file1.uuid());
+  }
+
+  @Test
+  public void deleteNonRootComponents_manual_measures_of_subviews_of_a_view() {
+    ComponentDto view = dbTester.components().insertView();
+    ComponentDto subview1 = dbTester.components().insertComponent(ComponentTesting.newSubView(view));
+    ComponentDto subview2 = dbTester.components().insertComponent(ComponentTesting.newSubView(subview1));
+    ComponentDto subview3 = dbTester.components().insertComponent(ComponentTesting.newSubView(view));
+    ComponentDto pc = dbTester.components().insertComponent(newProjectCopy("a", dbTester.components().insertPrivateProject(), view));
+    insertManualMeasureFor(view, subview1, subview2, subview3, pc);
+    assertThat(getComponentUuidsOfManualMeasures()).containsOnly(view.uuid(), subview1.uuid(), subview2.uuid(), subview3.uuid(), pc.uuid());
+
+    underTest.deleteNonRootComponents(dbSession, singletonList(subview1));
+    assertThat(getComponentUuidsOfManualMeasures())
+      .containsOnly(view.uuid(), subview2.uuid(), subview3.uuid(), pc.uuid());
+
+    underTest.deleteNonRootComponents(dbSession, asList(subview2, subview3, pc));
+    assertThat(getComponentUuidsOfManualMeasures())
+      .containsOnly(view.uuid(), pc.uuid());
+  }
+
+  private void insertManualMeasureFor(ComponentDto... componentDtos) {
+    Arrays.stream(componentDtos).forEach(componentDto -> dbClient.customMeasureDao().insert(dbSession, new CustomMeasureDto()
+      .setComponentUuid(componentDto.uuid())
+      .setMetricId(new Random().nextInt())));
+    dbSession.commit();
+  }
+
+  private Stream<String> getComponentUuidsOfManualMeasures() {
+    return dbTester.select("select component_uuid as \"COMPONENT_UUID\" from manual_measures").stream()
+      .map(row -> (String) row.get("COMPONENT_UUID"));
+  }
+
+  private Stream<Long> getResourceIdOfProperties() {
+    return dbTester.select("select resource_id as \"ID\" from properties").stream()
+      .map(row -> (Long) row.get("ID"));
+  }
+
+  private void insertPropertyFor(ComponentDto... components) {
+    Stream.of(components).forEach(componentDto -> dbTester.properties().insertProperty(new PropertyDto()
+      .setKey(randomAlphabetic(3))
+      .setValue(randomAlphabetic(3))
+      .setResourceId(componentDto.getId())));
+  }
+
+  private Stream<String> getComponentUuidsOfMeasures() {
+    return dbTester.select("select component_uuid as \"COMPONENT_UUID\" from project_measures").stream()
+      .map(row -> (String) row.get("COMPONENT_UUID"));
+  }
+
+  private void insertMeasureFor(ComponentDto... components) {
+    Arrays.stream(components).forEach(componentDto -> dbTester.getDbClient().measureDao().insert(dbSession, new MeasureDto()
+      .setMetricId(new Random().nextInt())
+      .setComponentUuid(componentDto.uuid())
+      .setAnalysisUuid(randomAlphabetic(3))));
+    dbSession.commit();
+  }
+
+  private Stream<String> getComponentUuidsOfFileSources() {
+    return dbTester.select("select file_uuid as \"COMPONENT_UUID\" from file_sources").stream()
+      .map(row -> (String) row.get("COMPONENT_UUID"));
+  }
+
+  private void insertFileSources(ComponentDto... components) {
+    Arrays.stream(components).forEach(component -> dbTester.getDbClient().fileSourceDao().insert(dbSession, new FileSourceDto()
+      .setFileUuid(component.uuid())
+      .setCreatedAt(new Random().nextInt())
+      .setUpdatedAt(new Random().nextInt())
+      .setProjectUuid(randomAlphabetic(3))
+      .setDataType("SOURCE")
+      .setBinaryData(new byte[] {0, 1})));
+    dbSession.commit();
+  }
+
+  private Stream<String> getComponentUuidsOfIssueChanges() {
+    return dbTester.select("select i.component_uuid as \"COMPONENT_UUID\" from issue_changes ic inner join issues i on i.kee = ic.issue_key").stream()
+      .map(row -> (String) row.get("COMPONENT_UUID"));
+  }
+
+  private void insertIssueAndChangesFor(ComponentDto... components) {
+    Arrays.stream(components).forEach(componentDto -> {
+      IssueDto issue = dbTester.issues().insert(RuleTesting.newRule(), componentDto, componentDto);
+      dbTester.issues().insertComment(issue, "foo", "bar");
+    });
+    dbSession.commit();
+  }
+
   private CeQueueDto createCeQueue(ComponentDto component, Status status) {
     CeQueueDto queueDto = new CeQueueDto();
     queueDto.setUuid(Uuids.create());
@@ -317,7 +713,7 @@ public class PurgeDaoTest {
   }
 
   private static PurgeConfiguration newConfigurationWith30Days(System2 system2, String... disabledComponentUuids) {
-    return new PurgeConfiguration(new IdUuidPair(THE_PROJECT_ID, THE_PROJECT_UUID), new String[0], 30, system2, Arrays.asList(disabledComponentUuids));
+    return new PurgeConfiguration(new IdUuidPair(THE_PROJECT_ID, THE_PROJECT_UUID), new String[0], 30, system2, asList(disabledComponentUuids));
   }
 
 }
index 27d0fb8fefc69e947e9b842391d01d2a8d7efa09..7dd68335f9a1d30935c5bdf185f15e8b78acc4eb 100644 (file)
@@ -57,7 +57,7 @@ public class ComponentCleanerService {
     if (hasNotProjectScope(project) || isNotDeletable(project)) {
       throw new IllegalArgumentException("Only projects can be deleted");
     }
-    dbClient.purgeDao().deleteProject(dbSession, project.uuid());
+    dbClient.purgeDao().deleteRootComponent(dbSession, project.uuid());
     dbSession.commit();
 
     deleteFromIndices(project.uuid());