]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19770 - Implement DAO query for issue list
authorantoine.vinot <antoine.vinot@sonarsource.com>
Tue, 4 Jul 2023 12:07:15 +0000 (14:07 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 19 Jul 2023 20:03:05 +0000 (20:03 +0000)
server/sonar-db-dao/src/it/java/org/sonar/db/issue/IssueDaoIT.java
server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java
server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueListQuery.java [new file with mode: 0644]
server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java
server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueTesting.java
server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml
server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIteratorForSingleChunk.java

index 332bb74f04edad3cc003452b52667b9c91c75f75..b4bd72da58bdf1db0b9d712fab6f95ad207b8601 100644 (file)
@@ -22,17 +22,22 @@ package org.sonar.db.issue;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Optional;
 import java.util.Set;
 import java.util.stream.IntStream;
 import java.util.stream.Stream;
 import javax.annotation.Nullable;
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.sonar.api.rule.RuleKey;
 import org.sonar.api.rules.RuleType;
 import org.sonar.api.utils.System2;
+import org.sonar.db.DbSession;
 import org.sonar.db.DbTester;
+import org.sonar.db.Pagination;
 import org.sonar.db.RowNotFoundException;
+import org.sonar.db.component.BranchDto;
 import org.sonar.db.component.BranchType;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.db.component.ComponentTesting;
@@ -45,8 +50,8 @@ import static java.util.Collections.emptyList;
 import static org.apache.commons.lang.math.RandomUtils.nextInt;
 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.assertj.core.api.AssertionsForClassTypes.tuple;
-import static org.junit.Assert.assertFalse;
 import static org.sonar.api.issue.Issue.RESOLUTION_FALSE_POSITIVE;
 import static org.sonar.api.issue.Issue.RESOLUTION_FIXED;
 import static org.sonar.api.issue.Issue.RESOLUTION_WONT_FIX;
@@ -56,7 +61,11 @@ import static org.sonar.api.issue.Issue.STATUS_OPEN;
 import static org.sonar.api.issue.Issue.STATUS_REOPENED;
 import static org.sonar.api.issue.Issue.STATUS_RESOLVED;
 import static org.sonar.api.issue.Issue.STATUS_REVIEWED;
+import static org.sonar.db.component.BranchType.BRANCH;
+import static org.sonar.db.component.BranchType.PULL_REQUEST;
 import static org.sonar.db.component.ComponentTesting.newFileDto;
+import static org.sonar.db.issue.IssueListQuery.IssueListQueryBuilder.newIssueListQueryBuilder;
+import static org.sonar.db.issue.IssueTesting.generateIssues;
 import static org.sonar.db.issue.IssueTesting.newCodeReferenceIssue;
 import static org.sonar.db.protobuf.DbIssues.MessageFormattingType.CODE;
 
@@ -87,53 +96,67 @@ public class IssueDaoIT {
 
   private final IssueDao underTest = db.getDbClient().issueDao();
 
+  private ComponentDto projectDto;
+
+  @Before
+  public void setup() {
+    db.rules().insert(RULE.setIsExternal(true));
+    projectDto = db.components().insertPrivateProject(t -> t.setUuid(PROJECT_UUID).setKey(PROJECT_KEY)).getMainBranchComponent();
+    db.components().insertComponent(newFileDto(projectDto).setUuid(FILE_UUID).setKey(FILE_KEY));
+  }
+
   @Test
   public void selectByKeyOrFail() {
     prepareTables();
+    IssueDto expected = new IssueDto()
+      .setKee(ISSUE_KEY1)
+      .setComponentUuid(FILE_UUID)
+      .setProjectUuid(PROJECT_UUID)
+      .setRuleUuid(RULE.getUuid())
+      .setLanguage(Optional.ofNullable(RULE.getLanguage()).orElseGet(() -> fail("Rule language should not be null here")))
+      .setSeverity("BLOCKER")
+      .setType(2)
+      .setManualSeverity(false)
+      .setMessage("the message")
+      .setRuleDescriptionContextKey(TEST_CONTEXT_KEY)
+      .setLine(500)
+      .setEffort(10L)
+      .setGap(3.14)
+      .setStatus("RESOLVED")
+      .setResolution("FIXED")
+      .setChecksum("123456789")
+      .setAuthorLogin("morgan")
+      .setAssigneeUuid("karadoc")
+      .setCreatedAt(1_440_000_000_000L)
+      .setUpdatedAt(1_440_000_000_000L)
+      .setRule(RULE)
+      .setComponentKey(FILE_KEY)
+      .setProjectKey(PROJECT_KEY)
+      .setExternal(true)
+      .setTags(List.of("tag1", "tag2"))
+      .setCodeVariants(List.of("variant1", "variant2"))
+      .setQuickFixAvailable(false)
+      .setMessageFormattings(MESSAGE_FORMATTING);
 
     IssueDto issue = underTest.selectOrFailByKey(db.getSession(), ISSUE_KEY1);
-    assertThat(issue.getKee()).isEqualTo(ISSUE_KEY1);
-    assertThat(issue.getComponentUuid()).isEqualTo(FILE_UUID);
-    assertThat(issue.getProjectUuid()).isEqualTo(PROJECT_UUID);
-    assertThat(issue.getRuleUuid()).isEqualTo(RULE.getUuid());
-    assertThat(issue.getLanguage()).isEqualTo(RULE.getLanguage());
-    assertThat(issue.getSeverity()).isEqualTo("BLOCKER");
-    assertThat(issue.getType()).isEqualTo(2);
-    assertThat(issue.isManualSeverity()).isFalse();
-    assertThat(issue.getMessage()).isEqualTo("the message");
+
+    assertThat(issue).usingRecursiveComparison()
+      .ignoringFields("filePath", "issueCreationDate", "issueUpdateDate", "issueCloseDate")
+      .isEqualTo(expected);
     assertThat(issue.parseMessageFormattings()).isEqualTo(MESSAGE_FORMATTING);
-    assertThat(issue.getOptionalRuleDescriptionContextKey()).contains(TEST_CONTEXT_KEY);
-    assertThat(issue.getLine()).isEqualTo(500);
-    assertThat(issue.getEffort()).isEqualTo(10L);
-    assertThat(issue.getGap()).isEqualTo(3.14);
-    assertThat(issue.getStatus()).isEqualTo("RESOLVED");
-    assertThat(issue.getResolution()).isEqualTo("FIXED");
-    assertThat(issue.getChecksum()).isEqualTo("123456789");
-    assertThat(issue.getAuthorLogin()).isEqualTo("morgan");
-    assertThat(issue.getAssigneeUuid()).isEqualTo("karadoc");
     assertThat(issue.getIssueCreationDate()).isNotNull();
     assertThat(issue.getIssueUpdateDate()).isNotNull();
     assertThat(issue.getIssueCloseDate()).isNotNull();
-    assertThat(issue.getCreatedAt()).isEqualTo(1_440_000_000_000L);
-    assertThat(issue.getUpdatedAt()).isEqualTo(1_440_000_000_000L);
     assertThat(issue.getRuleRepo()).isEqualTo(RULE.getRepositoryKey());
     assertThat(issue.getRule()).isEqualTo(RULE.getRuleKey());
-    assertThat(issue.getComponentKey()).isEqualTo(FILE_KEY);
-    assertThat(issue.getProjectKey()).isEqualTo(PROJECT_KEY);
-    assertThat(issue.getLocations()).isNull();
     assertThat(issue.parseLocations()).isNull();
-    assertThat(issue.isExternal()).isTrue();
-    assertThat(issue.getTags()).containsOnly("tag1", "tag2");
-    assertThat(issue.getCodeVariants()).containsOnly("variant1", "variant2");
-    assertFalse(issue.isQuickFixAvailable());
   }
 
   @Test
   public void selectByKeyOrFail_fails_if_key_not_found() {
-    assertThatThrownBy(() -> {
-      prepareTables();
-      underTest.selectOrFailByKey(db.getSession(), "DOES_NOT_EXIST");
-    })
+    prepareTables();
+    DbSession session = db.getSession();
+    assertThatThrownBy(() -> underTest.selectOrFailByKey(session, "DOES_NOT_EXIST"))
       .isInstanceOf(RowNotFoundException.class)
       .hasMessage("Issue with key 'DOES_NOT_EXIST' does not exist");
   }
@@ -237,7 +260,7 @@ public class IssueDaoIT {
     IntStream.range(0, statusesB.size()).forEach(i -> insertBranchIssue(branchB, fileB, rule, "B" + i, statusesB.get(i), updatedAt));
 
     List<IssueDto> branchAIssuesA1 = underTest.selectByBranch(db.getSession(), Set.of("issueA0", "issueA1", "issueA3", "issueWithResolution"),
-      buildSelectByBranchQuery(branchA, "java", false, changedSince));
+      buildSelectByBranchQuery(branchA, false, changedSince));
 
     assertThat(branchAIssuesA1)
       .extracting(IssueDto::getKey, IssueDto::getStatus, IssueDto::getResolution)
@@ -252,7 +275,7 @@ public class IssueDaoIT {
       .containsOnly("message", MESSAGE_FORMATTING);
 
     List<IssueDto> branchAIssuesA2 = underTest.selectByBranch(db.getSession(), Set.of("issueA0", "issueA1", "issueA3"),
-      buildSelectByBranchQuery(branchA, "java", true, changedSince));
+      buildSelectByBranchQuery(branchA, true, changedSince));
 
     assertThat(branchAIssuesA2)
       .extracting(IssueDto::getKey, IssueDto::getStatus)
@@ -260,7 +283,7 @@ public class IssueDaoIT {
         tuple("issueA1", STATUS_REVIEWED),
         tuple("issueA3", STATUS_RESOLVED));
 
-    List<IssueDto> branchBIssuesB1 = underTest.selectByBranch(db.getSession(), Set.of("issueB0", "issueB1"), buildSelectByBranchQuery(branchB, "java", false, changedSince));
+    List<IssueDto> branchBIssuesB1 = underTest.selectByBranch(db.getSession(), Set.of("issueB0", "issueB1"), buildSelectByBranchQuery(branchB, false, changedSince));
 
     assertThat(branchBIssuesB1)
       .extracting(IssueDto::getKey, IssueDto::getStatus)
@@ -268,7 +291,7 @@ public class IssueDaoIT {
         tuple("issueB0", STATUS_OPEN),
         tuple("issueB1", STATUS_RESOLVED));
 
-    List<IssueDto> branchBIssuesB2 = underTest.selectByBranch(db.getSession(), Set.of("issueB0", "issueB1"), buildSelectByBranchQuery(branchB, "java", true, changedSince));
+    List<IssueDto> branchBIssuesB2 = underTest.selectByBranch(db.getSession(), Set.of("issueB0", "issueB1"), buildSelectByBranchQuery(branchB, true, changedSince));
 
     assertThat(branchBIssuesB2)
       .extracting(IssueDto::getKey, IssueDto::getStatus)
@@ -298,7 +321,7 @@ public class IssueDaoIT {
     ComponentDto file = db.components().insertComponent(newFileDto(projectBranch));
 
     IssueDto openIssue = db.issues().insert(rule, projectBranch, file, i -> i.setStatus(STATUS_OPEN).setResolution(null));
-    IssueDto closedIssue = db.issues().insert(rule, projectBranch, file, i -> i.setStatus(STATUS_CLOSED).setResolution(RESOLUTION_FIXED));
+    db.issues().insert(rule, projectBranch, file, i -> i.setStatus(STATUS_CLOSED).setResolution(RESOLUTION_FIXED));
     IssueDto reopenedIssue = db.issues().insert(rule, projectBranch, file, i -> i.setStatus(STATUS_REOPENED).setResolution(null));
     IssueDto confirmedIssue = db.issues().insert(rule, projectBranch, file, i -> i.setStatus(STATUS_CONFIRMED).setResolution(null));
     IssueDto wontfixIssue = db.issues().insert(rule, projectBranch, file, i -> i.setStatus(STATUS_RESOLVED).setResolution(RESOLUTION_WONT_FIX));
@@ -350,7 +373,6 @@ public class IssueDaoIT {
 
   @Test
   public void selectByKey_givenOneIssueWithQuickFix_selectOneIssueWithQuickFix() {
-    prepareIssuesComponent();
     underTest.insert(db.getSession(), newIssueDto(ISSUE_KEY1)
       .setMessage("the message")
       .setRuleUuid(RULE.getUuid())
@@ -366,9 +388,7 @@ public class IssueDaoIT {
 
   @Test
   public void selectByKey_givenOneIssueWithoutQuickFix_selectOneIssueWithoutQuickFix() {
-    prepareIssuesComponent();
-    String issueKey = ISSUE_KEY1;
-    underTest.insert(db.getSession(), createIssueWithKey(issueKey));
+    underTest.insert(db.getSession(), createIssueWithKey(ISSUE_KEY1));
 
     IssueDto issue = underTest.selectOrFailByKey(db.getSession(), ISSUE_KEY1);
 
@@ -381,14 +401,14 @@ public class IssueDaoIT {
     ComponentDto project = db.components().insertPublicProject().getMainBranchComponent();
     ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(project));
     RuleDto rule = db.rules().insert();
-    IssueDto fpBug = db.issues().insert(rule, project, file,
+    db.issues().insert(rule, project, file,
       i -> i.setStatus("RESOLVED").setResolution("FALSE-POSITIVE").setSeverity("MAJOR").setType(RuleType.BUG).setIssueCreationTime(1_500L));
-    IssueDto criticalBug1 = db.issues().insert(rule, project, file,
+    db.issues().insert(rule, project, file,
       i -> i.setStatus("OPEN").setResolution(null).setSeverity("CRITICAL").setType(RuleType.BUG).setIssueCreationTime(1_600L));
     IssueDto criticalBug2 = db.issues().insert(rule, project, file,
       i -> i.setStatus("OPEN").setResolution(null).setSeverity("CRITICAL").setType(RuleType.BUG).setIssueCreationTime(1_700L));
     // closed issues are ignored
-    IssueDto closed = db.issues().insert(rule, project, file,
+    db.issues().insert(rule, project, file,
       i -> i.setStatus("CLOSED").setResolution("REMOVED").setSeverity("CRITICAL").setType(RuleType.BUG).setIssueCreationTime(1_700L));
 
     Collection<IssueGroupDto> result = underTest.selectIssueGroupsByComponent(db.getSession(), file, 1_000L);
@@ -469,7 +489,6 @@ public class IssueDaoIT {
 
   @Test
   public void selectByKey_givenOneIssueNewOnReferenceBranch_selectOneIssueWithNewOnReferenceBranch() {
-    prepareIssuesComponent();
     underTest.insert(db.getSession(), newIssueDto(ISSUE_KEY1)
       .setMessage("the message")
       .setRuleUuid(RULE.getUuid())
@@ -499,7 +518,6 @@ public class IssueDaoIT {
 
   @Test
   public void selectByKey_givenOneIssueWithoutRuleDescriptionContextKey_returnsEmptyOptional() {
-    prepareIssuesComponent();
     underTest.insert(db.getSession(), createIssueWithKey(ISSUE_KEY1)
       .setRuleDescriptionContextKey(null));
     IssueDto issue1 = underTest.selectOrFailByKey(db.getSession(), ISSUE_KEY1);
@@ -509,7 +527,6 @@ public class IssueDaoIT {
 
   @Test
   public void selectByKey_givenOneIssueWithRuleDescriptionContextKey_returnsContextKey() {
-    prepareIssuesComponent();
     underTest.insert(db.getSession(), createIssueWithKey(ISSUE_KEY1)
       .setRuleDescriptionContextKey(TEST_CONTEXT_KEY));
 
@@ -520,7 +537,6 @@ public class IssueDaoIT {
 
   @Test
   public void update_whenUpdatingRuleDescriptionContextKeyToNull_returnsEmptyContextKey() {
-    prepareIssuesComponent();
     IssueDto issue = createIssueWithKey(ISSUE_KEY1).setRuleDescriptionContextKey(TEST_CONTEXT_KEY);
     underTest.insert(db.getSession(), issue);
 
@@ -533,7 +549,6 @@ public class IssueDaoIT {
 
   @Test
   public void update_whenUpdatingRuleDescriptionContextKeyToNotNull_returnsContextKey() {
-    prepareIssuesComponent();
     IssueDto issue = createIssueWithKey(ISSUE_KEY1).setRuleDescriptionContextKey(null);
     underTest.insert(db.getSession(), issue);
 
@@ -546,7 +561,6 @@ public class IssueDaoIT {
 
   @Test
   public void update_givenOneIssueWithoutRuleDescriptionContextKey_returnsContextKey() {
-    prepareIssuesComponent();
     IssueDto issue = createIssueWithKey(ISSUE_KEY1).setRuleDescriptionContextKey(TEST_CONTEXT_KEY);
     underTest.insert(db.getSession(), issue);
 
@@ -563,12 +577,174 @@ public class IssueDaoIT {
     assertThat(issue.getOptionalRuleDescriptionContextKey()).contains(TEST_CONTEXT_KEY);
   }
 
+  @Test
+  public void selectByQuery_shouldBePaginated() {
+    List<IssueDto> issues = generateIssues(10, i -> createIssueWithKey("i-" + i));
+    issues.forEach(issue -> underTest.insert(db.getSession(), issue));
+
+    List<IssueDto> results = underTest.selectByQuery(
+      db.getSession(),
+      newIssueListQueryBuilder().project(PROJECT_KEY).build(),
+      Pagination.forPage(2).andSize(3));
+
+    List<String> expectedKeys = List.of("i-3", "i-4", "i-5");
+    assertThat(results.stream().map(IssueDto::getKey).toList()).containsExactlyElementsOf(expectedKeys);
+  }
+
+  @Test
+  public void selectByQuery_whenFilteredByBranch_shouldGetOnlyBranchIssues() {
+    BranchDto branchDto = ComponentTesting.newBranchDto(PROJECT_UUID, BRANCH);
+    ComponentDto branch = db.components().insertProjectBranch(projectDto, branchDto);
+    ComponentDto branchFile = db.components().insertComponent(newFileDto(branch));
+    List<IssueDto> mainBranchIssues = generateIssues(3, i -> createIssueWithKey("i-" + i));
+    List<IssueDto> otherBranchIssues = generateIssues(3, i -> createIssueWithKey("branch-" + i, branch.uuid(), branchFile.uuid()));
+    Stream.concat(mainBranchIssues.stream(), otherBranchIssues.stream())
+      .forEach(issue -> underTest.insert(db.getSession(), issue));
+
+    List<IssueDto> results = underTest.selectByQuery(
+      db.getSession(),
+      newIssueListQueryBuilder().project(PROJECT_KEY).branch(branchDto.getKey()).build(),
+      Pagination.forPage(1).andSize(6));
+
+    List<String> expectedKeys = List.of("branch-0", "branch-1", "branch-2");
+    assertThat(results.stream().map(IssueDto::getKey).toList()).containsExactlyElementsOf(expectedKeys);
+  }
+
+  @Test
+  public void selectByQuery_whenFilteredByPullRequest_shouldGetOnlyPRIssues() {
+    BranchDto pullRequestDto = ComponentTesting.newBranchDto(PROJECT_UUID, PULL_REQUEST);
+    ComponentDto branch = db.components().insertProjectBranch(projectDto, pullRequestDto);
+    ComponentDto branchFile = db.components().insertComponent(newFileDto(branch));
+    List<IssueDto> mainBranchIssues = generateIssues(3, i -> createIssueWithKey("i-" + i));
+    List<IssueDto> otherBranchIssues = generateIssues(3, i -> createIssueWithKey("pr-" + i, branch.uuid(), branchFile.uuid()));
+    Stream.concat(mainBranchIssues.stream(), otherBranchIssues.stream())
+      .forEach(issue -> underTest.insert(db.getSession(), issue));
+
+    List<IssueDto> results = underTest.selectByQuery(
+      db.getSession(),
+      newIssueListQueryBuilder().project(PROJECT_KEY).pullRequest(pullRequestDto.getKey()).build(),
+      Pagination.forPage(1).andSize(6));
+
+    List<String> expectedKeys = List.of("pr-0", "pr-1", "pr-2");
+    assertThat(results.stream().map(IssueDto::getKey).toList()).containsExactlyElementsOf(expectedKeys);
+  }
+
+  @Test
+  public void selectByQuery_whenFilteredByTypes_shouldGetIssuesWithSpecifiedTypes() {
+    List<IssueDto> bugs = generateIssues(3, i -> createIssueWithKey("bug-" + i).setType(RuleType.BUG));
+    List<IssueDto> codeSmells = generateIssues(3, i -> createIssueWithKey("codesmell-" + i).setType(RuleType.CODE_SMELL));
+    Stream.of(bugs, codeSmells)
+      .flatMap(Collection::stream)
+      .forEach(issue -> underTest.insert(db.getSession(), issue));
+
+    List<IssueDto> results = underTest.selectByQuery(
+      db.getSession(),
+      newIssueListQueryBuilder().project(PROJECT_KEY).types(List.of(RuleType.BUG.getDbConstant())).build(),
+      Pagination.forPage(1).andSize(10));
+
+    List<String> expectedKeys = List.of("bug-0", "bug-1", "bug-2");
+    assertThat(results.stream().map(IssueDto::getKey).toList()).containsExactlyElementsOf(expectedKeys);
+  }
+
+  @Test
+  public void selectByQuery_whenFilteredByFilteredStatuses_shouldGetIssuesWithoutSpecifiedStatuses() {
+    List<IssueDto> openIssues = generateIssues(3, i -> createIssueWithKey("open-" + i).setStatus("OPEN"));
+    List<IssueDto> closedIssues = generateIssues(3, i -> createIssueWithKey("closed-" + i).setStatus("CLOSED"));
+    List<IssueDto> resolvedIssues = generateIssues(3, i -> createIssueWithKey("resolved-" + i).setStatus("RESOLVED"));
+    Stream.of(openIssues, closedIssues, resolvedIssues)
+      .flatMap(Collection::stream)
+      .forEach(issue -> underTest.insert(db.getSession(), issue));
+
+    List<IssueDto> results = underTest.selectByQuery(
+      db.getSession(),
+      newIssueListQueryBuilder().project(PROJECT_KEY).statuses(List.of("OPEN")).build(),
+      Pagination.forPage(1).andSize(10));
+
+    List<String> expectedKeys = List.of("open-0", "open-1", "open-2");
+    assertThat(results.stream().map(IssueDto::getKey).toList()).containsExactlyElementsOf(expectedKeys);
+  }
+
+  @Test
+  public void selectByQuery_whenFilteredByFilteredResolutions_shouldGetIssuesWithoutSpecifiedResolution() {
+    List<IssueDto> unresolvedIssues = generateIssues(3, i -> createIssueWithKey("open-" + i).setResolution(null));
+    List<IssueDto> wontfixIssues = generateIssues(3, i -> createIssueWithKey("wf-" + i).setResolution("WONTFIX"));
+    List<IssueDto> falsePositiveIssues = generateIssues(3, i -> createIssueWithKey("fp-" + i).setResolution("FALSE-POSITIVE"));
+    Stream.of(unresolvedIssues, wontfixIssues, falsePositiveIssues)
+      .flatMap(Collection::stream)
+      .forEach(issue -> underTest.insert(db.getSession(), issue));
+
+    List<IssueDto> results = underTest.selectByQuery(
+      db.getSession(),
+      newIssueListQueryBuilder().project(PROJECT_KEY).resolutions(List.of("WONTFIX")).build(),
+      Pagination.forPage(1).andSize(10));
+
+    List<String> expectedKeys = List.of("wf-0", "wf-1", "wf-2");
+    assertThat(results.stream().map(IssueDto::getKey).toList()).containsExactlyElementsOf(expectedKeys);
+  }
+
+  @Test
+  public void selectByQuery_whenFilteredByFileComponent_shouldGetIssuesWithinFileOnly() {
+    ComponentDto otherFileDto = db.components().insertComponent(newFileDto(projectDto).setUuid("OTHER_UUID").setKey("OTHER_KEY"));
+    List<IssueDto> fromFileIssues = generateIssues(3, i -> createIssueWithKey("file-" + i));
+    List<IssueDto> fromOtherFileIssues = generateIssues(3, i -> createIssueWithKey("otherfile-" + i, PROJECT_UUID, otherFileDto.uuid()));
+    Stream.of(fromFileIssues, fromOtherFileIssues)
+      .flatMap(Collection::stream)
+      .forEach(issue -> underTest.insert(db.getSession(), issue));
+
+    List<IssueDto> results = underTest.selectByQuery(
+      db.getSession(),
+      newIssueListQueryBuilder().component(otherFileDto.getKey()).build(),
+      Pagination.forPage(1).andSize(10));
+
+    List<String> expectedKeys = List.of("otherfile-0", "otherfile-1", "otherfile-2");
+    assertThat(results.stream().map(IssueDto::getKey).toList()).containsExactlyElementsOf(expectedKeys);
+  }
+
+  @Test
+  public void selectByQuery_whenFilteredWithInNewCodeReference_shouldGetNewCodeReferenceIssues() {
+    List<IssueDto> issues = generateIssues(3, i -> createIssueWithKey("i-" + i));
+    List<IssueDto> newCodeRefIssues = generateIssues(3, i -> createIssueWithKey("newCodeRef-" + i));
+    Stream.of(issues, newCodeRefIssues)
+      .flatMap(Collection::stream)
+      .forEach(issue -> underTest.insert(db.getSession(), issue));
+    newCodeRefIssues.forEach(issue -> db.issues().insertNewCodeReferenceIssue(issue));
+
+    List<IssueDto> results = underTest.selectByQuery(
+      db.getSession(),
+      newIssueListQueryBuilder().project(PROJECT_KEY).newCodeOnReference(true).build(),
+      Pagination.forPage(1).andSize(10));
+
+    List<String> expectedKeys = List.of("newCodeRef-0", "newCodeRef-1", "newCodeRef-2");
+    assertThat(results.stream().map(IssueDto::getKey).toList()).containsExactlyElementsOf(expectedKeys);
+  }
+
+  @Test
+  public void selectByQuery_whenFilteredWithCreatedAfter_shouldGetIssuesCreatedAfterDate() {
+    List<IssueDto> createdBeforeIssues = generateIssues(3, i -> createIssueWithKey("createdBefore-" + i).setCreatedAt(1_400_000_000_000L));
+    List<IssueDto> createdAfterIssues = generateIssues(3, i -> createIssueWithKey("createdAfter-" + i).setCreatedAt(1_420_000_000_000L));
+    Stream.of(createdBeforeIssues, createdAfterIssues)
+      .flatMap(Collection::stream)
+      .forEach(issue -> underTest.insert(db.getSession(), issue));
+
+    List<IssueDto> results = underTest.selectByQuery(
+      db.getSession(),
+      newIssueListQueryBuilder().project(PROJECT_KEY).createdAfter(1_410_000_000_000L).build(),
+      Pagination.forPage(1).andSize(10));
+
+    List<String> expectedKeys = List.of("createdAfter-0", "createdAfter-1", "createdAfter-2");
+    assertThat(results.stream().map(IssueDto::getKey).toList()).containsExactlyElementsOf(expectedKeys);
+  }
+
   private static IssueDto createIssueWithKey(String issueKey) {
+    return createIssueWithKey(issueKey, PROJECT_UUID, FILE_UUID);
+  }
+
+  private static IssueDto createIssueWithKey(String issueKey, String branchUuid, String fileUuid) {
     return newIssueDto(issueKey)
       .setMessage("the message")
       .setRuleUuid(RULE.getUuid())
-      .setComponentUuid(FILE_UUID)
-      .setProjectUuid(PROJECT_UUID)
+      .setComponentUuid(fileUuid)
+      .setProjectUuid(branchUuid)
       .setQuickFixAvailable(false);
   }
 
@@ -600,15 +776,7 @@ public class IssueDaoIT {
     dto.setCodeVariants(Set.of("variant1", "variant2"));
     return dto;
   }
-
-  private void prepareIssuesComponent() {
-    db.rules().insert(RULE.setIsExternal(true));
-    ComponentDto projectDto = db.components().insertPrivateProject(t -> t.setUuid(PROJECT_UUID).setKey(PROJECT_KEY)).getMainBranchComponent();
-    db.components().insertComponent(newFileDto(projectDto).setUuid(FILE_UUID).setKey(FILE_KEY));
-  }
-
   private void prepareTables() {
-    prepareIssuesComponent();
     underTest.insert(db.getSession(), newIssueDto(ISSUE_KEY1)
       .setMessage("the message")
       .setRuleUuid(RULE.getUuid())
@@ -640,7 +808,7 @@ public class IssueDaoIT {
     insertBranchIssue(branch, file, rule, id, status, null, updateAt);
   }
 
-  private static IssueQueryParams buildSelectByBranchQuery(ComponentDto branch, String language, boolean resolvedOnly, Long changedSince) {
-    return new IssueQueryParams(branch.uuid(), List.of(language), List.of(), List.of(), resolvedOnly, changedSince);
+  private static IssueQueryParams buildSelectByBranchQuery(ComponentDto branch, boolean resolvedOnly, Long changedSince) {
+    return new IssueQueryParams(branch.uuid(), List.of("java"), List.of(), List.of(), resolvedOnly, changedSince);
   }
 }
index 1ac18451d3310d8cf3c188d086260e269c6b6a64..679cf8c0e3483e9eb78cfaca3f5a5e31a753ea6b 100644 (file)
@@ -126,4 +126,9 @@ public class IssueDao implements Dao {
   public List<String> selectRecentlyClosedIssues(DbSession dbSession, IssueQueryParams issueQueryParams) {
     return mapper(dbSession).selectRecentlyClosedIssues(issueQueryParams);
   }
+
+  public List<IssueDto> selectByQuery(DbSession dbSession, IssueListQuery issueListQuery, Pagination pagination) {
+    return mapper(dbSession).selectByQuery(issueListQuery, pagination);
+  }
+
 }
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueListQuery.java b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueListQuery.java
new file mode 100644 (file)
index 0000000..f4b4f8e
--- /dev/null
@@ -0,0 +1,173 @@
+/*
+ * 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.db.issue;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import static java.util.Collections.emptyList;
+import static java.util.Optional.ofNullable;
+
+public class IssueListQuery {
+
+  private final String project;
+  private final String branch;
+  private final String pullRequest;
+  private final String component;
+  private final Boolean resolved;
+  private final Long createdAfter;
+  private final boolean newCodeOnReference;
+  private final Collection<Integer> types;
+  private final Collection<String> statuses;
+  private final Collection<String> resolutions;
+
+  private IssueListQuery(IssueListQueryBuilder issueListQueryBuilder) {
+    this.project = issueListQueryBuilder.project;
+    this.branch = issueListQueryBuilder.branch;
+    this.pullRequest = issueListQueryBuilder.pullRequest;
+    this.component = issueListQueryBuilder.component;
+    this.resolved = issueListQueryBuilder.resolved;
+    this.createdAfter = issueListQueryBuilder.createdAfter;
+    this.newCodeOnReference = issueListQueryBuilder.newCodeOnReference;
+    this.types = ofNullable(issueListQueryBuilder.types)
+      .map(Collections::unmodifiableCollection)
+      .orElse(emptyList());
+    this.statuses = ofNullable(issueListQueryBuilder.statuses)
+      .map(Collections::unmodifiableCollection)
+      .orElse(emptyList());
+    this.resolutions = ofNullable(issueListQueryBuilder.resolutions)
+      .map(Collections::unmodifiableCollection)
+      .orElse(emptyList());
+  }
+
+  public String getProject() {
+    return project;
+  }
+
+  public String getBranch() {
+    return branch;
+  }
+
+  public String getPullRequest() {
+    return pullRequest;
+  }
+
+  public String getComponent() {
+    return component;
+  }
+
+  public Boolean getResolved() {
+    return resolved;
+  }
+
+  public Long getCreatedAfter() {
+    return createdAfter;
+  }
+
+  public boolean getNewCodeOnReference() {
+    return newCodeOnReference;
+  }
+
+  public Collection<Integer> getTypes() {
+    return types;
+  }
+
+  public Collection<String> getStatuses() {
+    return statuses;
+  }
+
+  public Collection<String> getResolutions() {
+    return resolutions;
+  }
+
+  public static final class IssueListQueryBuilder {
+    private String project;
+    private String branch;
+    private String pullRequest;
+    private String component;
+    private Boolean resolved;
+    private Long createdAfter;
+    private boolean newCodeOnReference;
+    private Collection<Integer> types;
+    private Collection<String> statuses;
+    private Collection<String> resolutions;
+
+    private IssueListQueryBuilder() {
+    }
+
+    public static IssueListQueryBuilder newIssueListQueryBuilder() {
+      return new IssueListQueryBuilder();
+    }
+
+    public IssueListQueryBuilder project(String project) {
+      this.project = project;
+      return this;
+    }
+
+    public IssueListQueryBuilder branch(String branch) {
+      this.branch = branch;
+      return this;
+    }
+
+    public IssueListQueryBuilder pullRequest(String pullRequest) {
+      this.pullRequest = pullRequest;
+      return this;
+    }
+
+    public IssueListQueryBuilder component(String component) {
+      this.component = component;
+      return this;
+    }
+
+    public IssueListQueryBuilder resolved(Boolean resolved) {
+      this.resolved = resolved;
+      return this;
+    }
+
+    public IssueListQueryBuilder createdAfter(Long createdAfter) {
+      this.createdAfter = createdAfter;
+      return this;
+    }
+
+    public IssueListQueryBuilder newCodeOnReference(boolean newCodeOnReference) {
+      this.newCodeOnReference = newCodeOnReference;
+      return this;
+    }
+
+    public IssueListQueryBuilder types(Collection<Integer> types) {
+      this.types = types;
+      return this;
+    }
+
+    public IssueListQueryBuilder statuses(Collection<String> statuses) {
+      this.statuses = statuses;
+      return this;
+    }
+
+    public IssueListQueryBuilder resolutions(Collection<String> resolutions) {
+      this.resolutions = resolutions;
+      return this;
+    }
+
+    public IssueListQuery build() {
+      return new IssueListQuery(this);
+    }
+  }
+}
index 9475fec4707f7cbf7c5bc490575e24c18d12a9d0..3238a8a089fc40ed85d75fe5509179a0320b7631 100644 (file)
@@ -76,4 +76,6 @@ public interface IssueMapper {
 
 
   List<String> selectRecentlyClosedIssues(@Param("queryParams") IssueQueryParams issueQueryParams);
+
+  List<IssueDto> selectByQuery(@Param("query") IssueListQuery issueListQuery, @Param("pagination") Pagination pagination);
 }
index 957adc5fda06cdd3111acd771df1ef6a23545294..201fbff439d31cac57ae52c6c2595b538acc15b0 100644 (file)
@@ -20,6 +20,9 @@
 package org.sonar.db.issue;
 
 import java.util.Date;
+import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Stream;
 import org.apache.commons.lang.math.RandomUtils;
 import org.sonar.api.issue.Issue;
 import org.sonar.api.resources.Qualifiers;
@@ -101,4 +104,11 @@ public class IssueTesting {
       .setCreatedAt(1_400_000_000_000L);
   }
 
+  public static List<IssueDto> generateIssues(int total, Function<Integer, IssueDto> issueGenerator) {
+    return Stream.iterate(0, i -> i + 1)
+      .map(issueGenerator)
+      .limit(total)
+      .toList();
+  }
+
 }
index d1d0849563e692e99223d49c35baccb410e39016..a8a33de5f151e7133a127f535cd33ddab2cfb25b 100644 (file)
         </if>
   </select>
 
+  <select id="selectByQuery" parameterType="map" resultType="Issue">
+    select
+    <include refid="issueColumns"/>
+    from issues i
+    inner join rules r on r.uuid=i.rule_uuid
+    inner join components p on p.uuid=i.component_uuid
+    inner join components root on root.uuid=i.project_uuid
+    inner join project_branches pb on pb.uuid=i.project_uuid
+    left join new_code_reference_issues n on i.kee = n.issue_key
+    <where>
+      <if test="query.project != null">
+        root.kee = #{query.project,jdbcType=VARCHAR}
+      </if>
+      <if test="query.component != null">
+        AND p.kee = #{query.component,jdbcType=VARCHAR}
+      </if>
+      <if test="query.branch != null">
+        AND pb.kee = #{query.branch,jdbcType=VARCHAR}
+      </if>
+      <if test="query.pullRequest != null">
+        AND pb.kee = #{query.pullRequest,jdbcType=VARCHAR}
+      </if>
+      <if test="query.types.size() > 0">
+        AND i.issue_type IN
+        <foreach collection="query.types" open="(" close=")" item="type" separator=",">
+          #{type,jdbcType=VARCHAR}
+        </foreach>
+      </if>
+      <if test="query.statuses.size() > 0">
+        AND i.status IN
+        <foreach collection="query.statuses" open="(" close=")" item="status" separator=",">
+          #{status,jdbcType=VARCHAR}
+        </foreach>
+      </if>
+      <if test="query.resolutions.size() > 0">
+        AND i.resolution IN
+        <foreach collection="query.resolutions" open="(" close=")" item="resolution" separator=",">
+          #{resolution,jdbcType=VARCHAR}
+        </foreach>
+      </if>
+      <if test="query.newCodeOnReference == true">
+        AND n.uuid IS NOT NULL
+      </if>
+      <if test="query.createdAfter != null">
+        AND i.created_at &gt;= #{query.createdAfter,jdbcType=BIGINT}
+      </if>
+    </where>
+    order by i.kee asc
+    offset (#{pagination.startRowNumber,jdbcType=INTEGER}-1) rows fetch next #{pagination.pageSize,jdbcType=INTEGER} rows only
+  </select>
+
 </mapper>
index 213a55599e16246018b1cbdaaa1c543e39e58f5a..86d2196a20ee93a5562c27115f24fe6dd100ddbf 100644 (file)
@@ -54,7 +54,6 @@ import static org.sonar.server.security.SecurityStandards.fromSecurityStandards;
 class IssueIteratorForSingleChunk implements IssueIterator {
 
   private static final String[] FIELDS = {
-    // column 1
     "i.kee",
     "i.assignee",
     "i.line",
@@ -65,8 +64,6 @@ class IssueIteratorForSingleChunk implements IssueIterator {
     "i.author_login",
     "i.issue_close_date",
     "i.issue_creation_date",
-
-    // column 11
     "i.issue_update_date",
     "r.uuid",
     "r.language",
@@ -76,8 +73,6 @@ class IssueIteratorForSingleChunk implements IssueIterator {
     "c.branch_uuid",
     "pb.is_main",
     "pb.project_uuid",
-
-    // column 22
     "i.tags",
     "i.issue_type",
     "r.security_standards",