From 8ada0c710270884f3936b8d51211d74ece8c2a1c Mon Sep 17 00:00:00 2001 From: "antoine.vinot" Date: Tue, 4 Jul 2023 14:07:15 +0200 Subject: [PATCH] SONAR-19770 - Implement DAO query for issue list --- .../java/org/sonar/db/issue/IssueDaoIT.java | 292 ++++++++++++++---- .../java/org/sonar/db/issue/IssueDao.java | 5 + .../org/sonar/db/issue/IssueListQuery.java | 173 +++++++++++ .../java/org/sonar/db/issue/IssueMapper.java | 2 + .../java/org/sonar/db/issue/IssueTesting.java | 10 + .../org/sonar/db/issue/IssueMapper.xml | 51 +++ .../index/IssueIteratorForSingleChunk.java | 5 - 7 files changed, 471 insertions(+), 67 deletions(-) create mode 100644 server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueListQuery.java diff --git a/server/sonar-db-dao/src/it/java/org/sonar/db/issue/IssueDaoIT.java b/server/sonar-db-dao/src/it/java/org/sonar/db/issue/IssueDaoIT.java index 332bb74f04e..b4bd72da58b 100644 --- a/server/sonar-db-dao/src/it/java/org/sonar/db/issue/IssueDaoIT.java +++ b/server/sonar-db-dao/src/it/java/org/sonar/db/issue/IssueDaoIT.java @@ -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 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 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 branchBIssuesB1 = underTest.selectByBranch(db.getSession(), Set.of("issueB0", "issueB1"), buildSelectByBranchQuery(branchB, "java", false, changedSince)); + List 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 branchBIssuesB2 = underTest.selectByBranch(db.getSession(), Set.of("issueB0", "issueB1"), buildSelectByBranchQuery(branchB, "java", true, changedSince)); + List 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 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 issues = generateIssues(10, i -> createIssueWithKey("i-" + i)); + issues.forEach(issue -> underTest.insert(db.getSession(), issue)); + + List results = underTest.selectByQuery( + db.getSession(), + newIssueListQueryBuilder().project(PROJECT_KEY).build(), + Pagination.forPage(2).andSize(3)); + + List 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 mainBranchIssues = generateIssues(3, i -> createIssueWithKey("i-" + i)); + List 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 results = underTest.selectByQuery( + db.getSession(), + newIssueListQueryBuilder().project(PROJECT_KEY).branch(branchDto.getKey()).build(), + Pagination.forPage(1).andSize(6)); + + List 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 mainBranchIssues = generateIssues(3, i -> createIssueWithKey("i-" + i)); + List 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 results = underTest.selectByQuery( + db.getSession(), + newIssueListQueryBuilder().project(PROJECT_KEY).pullRequest(pullRequestDto.getKey()).build(), + Pagination.forPage(1).andSize(6)); + + List 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 bugs = generateIssues(3, i -> createIssueWithKey("bug-" + i).setType(RuleType.BUG)); + List 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 results = underTest.selectByQuery( + db.getSession(), + newIssueListQueryBuilder().project(PROJECT_KEY).types(List.of(RuleType.BUG.getDbConstant())).build(), + Pagination.forPage(1).andSize(10)); + + List 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 openIssues = generateIssues(3, i -> createIssueWithKey("open-" + i).setStatus("OPEN")); + List closedIssues = generateIssues(3, i -> createIssueWithKey("closed-" + i).setStatus("CLOSED")); + List 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 results = underTest.selectByQuery( + db.getSession(), + newIssueListQueryBuilder().project(PROJECT_KEY).statuses(List.of("OPEN")).build(), + Pagination.forPage(1).andSize(10)); + + List 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 unresolvedIssues = generateIssues(3, i -> createIssueWithKey("open-" + i).setResolution(null)); + List wontfixIssues = generateIssues(3, i -> createIssueWithKey("wf-" + i).setResolution("WONTFIX")); + List 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 results = underTest.selectByQuery( + db.getSession(), + newIssueListQueryBuilder().project(PROJECT_KEY).resolutions(List.of("WONTFIX")).build(), + Pagination.forPage(1).andSize(10)); + + List 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 fromFileIssues = generateIssues(3, i -> createIssueWithKey("file-" + i)); + List 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 results = underTest.selectByQuery( + db.getSession(), + newIssueListQueryBuilder().component(otherFileDto.getKey()).build(), + Pagination.forPage(1).andSize(10)); + + List 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 issues = generateIssues(3, i -> createIssueWithKey("i-" + i)); + List 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 results = underTest.selectByQuery( + db.getSession(), + newIssueListQueryBuilder().project(PROJECT_KEY).newCodeOnReference(true).build(), + Pagination.forPage(1).andSize(10)); + + List 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 createdBeforeIssues = generateIssues(3, i -> createIssueWithKey("createdBefore-" + i).setCreatedAt(1_400_000_000_000L)); + List 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 results = underTest.selectByQuery( + db.getSession(), + newIssueListQueryBuilder().project(PROJECT_KEY).createdAfter(1_410_000_000_000L).build(), + Pagination.forPage(1).andSize(10)); + + List 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); } } diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java index 1ac18451d33..679cf8c0e34 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java @@ -126,4 +126,9 @@ public class IssueDao implements Dao { public List selectRecentlyClosedIssues(DbSession dbSession, IssueQueryParams issueQueryParams) { return mapper(dbSession).selectRecentlyClosedIssues(issueQueryParams); } + + public List 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 index 00000000000..f4b4f8e0337 --- /dev/null +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueListQuery.java @@ -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 types; + private final Collection statuses; + private final Collection 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 getTypes() { + return types; + } + + public Collection getStatuses() { + return statuses; + } + + public Collection 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 types; + private Collection statuses; + private Collection 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 types) { + this.types = types; + return this; + } + + public IssueListQueryBuilder statuses(Collection statuses) { + this.statuses = statuses; + return this; + } + + public IssueListQueryBuilder resolutions(Collection resolutions) { + this.resolutions = resolutions; + return this; + } + + public IssueListQuery build() { + return new IssueListQuery(this); + } + } +} diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java index 9475fec4707..3238a8a089f 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java @@ -76,4 +76,6 @@ public interface IssueMapper { List selectRecentlyClosedIssues(@Param("queryParams") IssueQueryParams issueQueryParams); + + List selectByQuery(@Param("query") IssueListQuery issueListQuery, @Param("pagination") Pagination pagination); } diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueTesting.java b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueTesting.java index 957adc5fda0..201fbff439d 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueTesting.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueTesting.java @@ -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 generateIssues(int total, Function issueGenerator) { + return Stream.iterate(0, i -> i + 1) + .map(issueGenerator) + .limit(total) + .toList(); + } + } diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml index d1d0849563e..a8a33de5f15 100644 --- a/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml +++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml @@ -718,4 +718,55 @@ + + diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIteratorForSingleChunk.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIteratorForSingleChunk.java index 213a55599e1..86d2196a20e 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIteratorForSingleChunk.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIteratorForSingleChunk.java @@ -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", -- 2.39.5