return mapper(session).selectIssueKeysByComponentUuid(componentUuid);
}
+ public Set<String> selectIssueKeysByComponentUuidAndChangedSinceDate(DbSession session, String componentUuid, long changedSince) {
+ return mapper(session).selectIssueKeysByComponentUuidAndChangedSinceDate(componentUuid, changedSince);
+ }
+
public List<IssueDto> selectByComponentUuidPaginated(DbSession session, String componentUuid, int page) {
return mapper(session).selectByComponentUuidPaginated(componentUuid, Pagination.forPage(page).andSize(DEFAULT_PAGE_SIZE));
}
Set<String> selectIssueKeysByComponentUuid(@Param("componentUuid") String componentUuid);
+ Set<String> selectIssueKeysByComponentUuidAndChangedSinceDate(@Param("componentUuid") String componentUuid, @Param("changedSince") long changedSince);
+
List<IssueDto> selectByComponentUuidPaginated(@Param("componentUuid") String componentUuid,
@Param("pagination") Pagination pagination);
i.component_uuid = #{componentUuid,jdbcType=VARCHAR}
and i.status = 'CLOSED'
and i.issue_close_date is not null
- and i.issue_close_date >= #{closeDateAfter,jdbcType=BIGINT}
+ and i.issue_close_date >= #{closeDateAfter,jdbcType=BIGINT}
order by
i.kee, ic.issue_change_creation_date desc
</select>
i.project_uuid=#{componentUuid,jdbcType=VARCHAR}
</select>
+ <select id="selectIssueKeysByComponentUuidAndChangedSinceDate" parameterType="map" resultType="string">
+ select
+ i.kee
+ from issues i
+ where
+ i.project_uuid=#{componentUuid,jdbcType=VARCHAR}
+ AND i.issue_update_date >= #{changedSince,jdbcType=BIGINT}
+ </select>
+
<select id="selectByComponentUuidPaginated" parameterType="map" resultType="Issue">
select
<include refid="issueColumns"/>,
#{language}
</foreach>
</if>
- order by i.issue_creation_date ASC
+ order by i.kee ASC
limit #{pagination.pageSize,jdbcType=INTEGER} offset #{pagination.offset,jdbcType=INTEGER}
</select>
<include refid="selectByBranchColumnsOuterQuery"/>
from (
select
- row_number() over(order by i.issue_creation_date ASC) as row_number,
+ row_number() over(order by i.kee ASC) as row_number,
<include refid="selectByBranchColumns"/>
from issues i
inner join project_branches b on i.project_uuid = b.project_uuid
#{language}
</foreach>
</if>
- order by i.issue_creation_date ASC
+ order by i.kee ASC
) a
) t
where
import java.util.Collections;
import java.util.List;
import java.util.Set;
+import java.util.stream.IntStream;
import java.util.stream.Stream;
import org.junit.Rule;
import org.junit.Test;
assertThat(issues).containsOnly("I1", "I2");
}
+ @Test
+ public void selectIssueKeysByComponentUuidAndChangedSince() {
+ long t1 = 1_340_000_000_000L;
+ long t2 = 1_400_000_000_000L;
+ // contains I1 and I2
+ prepareTables();
+ // Insert I3, I4, where t1 < t2
+ IntStream.range(3, 5).forEach(i -> underTest.insert(db.getSession(), newIssueDto("I" + i).setUpdatedAt(t1)));
+
+ Set<String> issues = underTest.selectIssueKeysByComponentUuidAndChangedSinceDate(db.getSession(), PROJECT_UUID, t2);
+
+ // results are not ordered, so do not use "containsExactly"
+ assertThat(issues).containsOnly("I1", "I2");
+ }
+
@Test
public void selectByComponentUuidPaginated() {
// contains I1 and I2
import java.io.IOException;
import java.io.OutputStream;
+import java.util.HashSet;
import java.util.List;
import java.util.Optional;
+import java.util.Set;
import java.util.function.Consumer;
import javax.annotation.Nullable;
import org.sonar.api.server.ws.Request;
import org.sonar.server.issue.ws.pull.PullActionResponseWriter;
import org.sonar.server.user.UserSession;
+import static java.util.Optional.*;
import static org.sonar.api.web.UserRole.USER;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_PULL;
pullActionResponseWriter.appendTimestampToResponse(outputStream);
var pullActionQueryParams = new IssueQueryParams(projectDto.get().getUuid(), branchName,
languages, ruleRepositories, resolvedOnly, changedSince);
- retrieveAndSendIssues(dbSession, pullActionQueryParams, outputStream);
+ retrieveAndSendIssues(dbSession, projectDto.get().getUuid(), pullActionQueryParams, outputStream);
}
}
userSession.checkProjectPermission(USER, projectDto.get());
}
- private void retrieveAndSendIssues(DbSession dbSession, IssueQueryParams queryParams, OutputStream outputStream)
+ private void retrieveAndSendIssues(DbSession dbSession, String componentUuid, IssueQueryParams queryParams, OutputStream outputStream)
throws IOException {
var issuesRetriever = new PullActionIssuesRetriever(dbClient, queryParams);
+ Set<String> issueKeysSnapshot = new HashSet<>(getIssueKeysSnapshot(componentUuid, queryParams.getChangedSince()));
Consumer<List<IssueDto>> listConsumer = issueDtos -> pullActionResponseWriter.appendIssuesToResponse(issueDtos, outputStream);
- issuesRetriever.processIssuesByBatch(dbSession, listConsumer);
+ issuesRetriever.processIssuesByBatch(dbSession, issueKeysSnapshot, listConsumer);
if (queryParams.getChangedSince() != null) {
// in the "incremental mode" we need to send SonarLint also recently closed issues keys
pullActionResponseWriter.appendClosedIssuesUuidsToResponse(closedIssues, outputStream);
}
}
+
+ private Set<String> getIssueKeysSnapshot(String componentUuid, @Nullable Long changedSince) {
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ Optional<Long> changedSinceDate = ofNullable(changedSince);
+
+ if (changedSinceDate.isPresent()) {
+ return dbClient.issueDao().selectIssueKeysByComponentUuidAndChangedSinceDate(dbSession, componentUuid, changedSinceDate.get());
+ }
+
+ return dbClient.issueDao().selectIssueKeysByComponentUuid(dbSession, componentUuid);
+ }
+ }
}
package org.sonar.server.issue.ws.pull;
import java.util.List;
+import java.util.Set;
import java.util.function.Consumer;
+import java.util.stream.Collectors;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.issue.IssueDto;
this.issueQueryParams = queryParams;
}
- public void processIssuesByBatch(DbSession dbSession, Consumer<List<IssueDto>> listConsumer) {
+ public void processIssuesByBatch(DbSession dbSession, Set<String> issueKeysSnapshot, Consumer<List<IssueDto>> listConsumer) {
int nextPage = 1;
boolean hasMoreIssues = true;
while (hasMoreIssues) {
List<IssueDto> issueDtos = nextOpenIssues(dbSession, nextPage);
- listConsumer.accept(issueDtos);
+ listConsumer.accept(filterDuplicateIssues(issueDtos, issueKeysSnapshot));
nextPage++;
if (issueDtos.isEmpty() || issueDtos.size() < DEFAULT_PAGE_SIZE) {
hasMoreIssues = false;
}
}
+ private static List<IssueDto> filterDuplicateIssues(List<IssueDto> issues, Set<String> issueKeysSnapshot) {
+ return issues
+ .stream()
+ .filter(issue -> isUniqueIssue(issue.getKee(), issueKeysSnapshot))
+ .collect(Collectors.toList());
+ }
+
+ private static boolean isUniqueIssue(String issueKey, Set<String> issueKeysSnapshot) {
+ boolean isUniqueIssue = issueKeysSnapshot.contains(issueKey);
+
+ if (isUniqueIssue) {
+ issueKeysSnapshot.remove(issueKey);
+ }
+
+ return isUniqueIssue;
+ }
+
public List<String> retrieveClosedIssues(DbSession dbSession) {
return dbClient.issueDao().selectRecentlyClosedIssues(dbSession, issueQueryParams);
}
import java.util.ArrayList;
import java.util.List;
+import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
List<IssueDto> returnedDtos = new ArrayList<>();
Consumer<List<IssueDto>> listConsumer = returnedDtos::addAll;
- pullActionIssuesRetriever.processIssuesByBatch(dbClient.openSession(true), listConsumer);
+ pullActionIssuesRetriever.processIssuesByBatch(dbClient.openSession(true), Set.of(), listConsumer);
assertThat(returnedDtos).isEmpty();
}
@Test
public void processIssuesByBatch_givenThousandOneIssuesReturnedByDatabase_thousandOneIssuesConsumed() {
var pullActionIssuesRetriever = new PullActionIssuesRetriever(dbClient, queryParams);
- List<IssueDto> thousandIssues = IntStream.rangeClosed(1, 1000).mapToObj(i -> new IssueDto()).collect(Collectors.toList());
+ List<IssueDto> thousandIssues = IntStream.rangeClosed(1, 1000).mapToObj(i -> new IssueDto().setKee(Integer.toString(i))).collect(Collectors.toList());
+ IssueDto singleIssue = new IssueDto().setKee("kee");
when(issueDao.selectByBranch(any(), any(), anyInt()))
.thenReturn(thousandIssues)
- .thenReturn(List.of(new IssueDto()));
+ .thenReturn(List.of(singleIssue));
List<IssueDto> returnedDtos = new ArrayList<>();
Consumer<List<IssueDto>> listConsumer = returnedDtos::addAll;
- pullActionIssuesRetriever.processIssuesByBatch(dbClient.openSession(true), listConsumer);
+ Set<String> thousandIssueUuidsSnapshot = thousandIssues.stream().map(IssueDto::getKee).collect(Collectors.toSet());
+ thousandIssueUuidsSnapshot.add(singleIssue.getKee());
+ pullActionIssuesRetriever.processIssuesByBatch(dbClient.openSession(true), thousandIssueUuidsSnapshot, listConsumer);
assertThat(returnedDtos).hasSize(1001);
}
+
+ @Test
+ public void processIssuesByBatch_filter_out_duplicate_issue_entries() {
+ var pullActionIssuesRetriever = new PullActionIssuesRetriever(dbClient, queryParams);
+ IssueDto issue1 = new IssueDto().setKee("kee1");
+ IssueDto issue2 = new IssueDto().setKee("kee2");
+ List<IssueDto> issues = List.of(issue1, issue1, issue1, issue2);
+ when(issueDao.selectByBranch(any(), any(), anyInt()))
+ .thenReturn(issues);
+ List<IssueDto> returnedDtos = new ArrayList<>();
+ Consumer<List<IssueDto>> listConsumer = returnedDtos::addAll;
+
+ Set<String> thousandIssueKeysSnapshot = issues.stream().map(IssueDto::getKee).collect(Collectors.toSet());
+ pullActionIssuesRetriever.processIssuesByBatch(dbClient.openSession(true), thousandIssueKeysSnapshot, listConsumer);
+
+ assertThat(returnedDtos)
+ .hasSize(2)
+ .containsExactlyInAnyOrder(issue1, issue2);
+ }
+
+ @Test
+ public void processIssuesByBatch_correctly_processes_all_issues_regardless_of_creation_timestamp() {
+ var pullActionIssuesRetriever = new PullActionIssuesRetriever(dbClient, queryParams);
+ List<IssueDto> issuesWithSameCreationTimestamp = IntStream.rangeClosed(1, 100).mapToObj(i -> new IssueDto().setKee(Integer.toString(i)).setCreatedAt(100L)).collect(Collectors.toList());
+ when(issueDao.selectByBranch(any(), any(), anyInt()))
+ .thenReturn(issuesWithSameCreationTimestamp);
+ List<IssueDto> returnedDtos = new ArrayList<>();
+ Consumer<List<IssueDto>> listConsumer = returnedDtos::addAll;
+
+ Set<String> issueKeysSnapshot = issuesWithSameCreationTimestamp.stream().map(IssueDto::getKee).collect(Collectors.toSet());
+ pullActionIssuesRetriever.processIssuesByBatch(dbClient.openSession(true), issueKeysSnapshot, listConsumer);
+
+ assertThat(returnedDtos).hasSize(100);
+ }
}