Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ and c.message like concat('%#', :prNumber, '%')
// The bot filter is needed because of that email branch: an automation account configured
// with the user's own address satisfies it. No null guard is needed on author_name; the
// column is NOT NULL.
//
// Windows are half-open: `from` inclusive, `to` exclusive, so adjacent windows neither
// double-count a record nor drop one. Callers pass toDate + 1 at midnight UTC.
// -------------------------------------------------------------------------

@Query("""
Expand All @@ -70,7 +73,8 @@ select date(c.authorDate) as day,
where r.id IN :repoIds
and (c.authorGithubId = :githubUserId or lower(c.authorEmail) in :emails)
and c.authorName not like '%[bot]%'
and c.authorDate between :from and :to
and c.authorDate >= :from
and c.authorDate < :to
group by date(c.authorDate), r.id
order by day, repoId
""")
Expand All @@ -91,7 +95,8 @@ select date(c.authorDate) as day,
where r.id IN :repoIds
and (c.authorGithubId = :githubUserId or lower(c.authorEmail) in :emails)
and c.authorName not like '%[bot]%'
and c.authorDate between :from and :to
and c.authorDate >= :from
and c.authorDate < :to
group by date(c.authorDate), r.id
order by day, repoId
""")
Expand Down Expand Up @@ -129,7 +134,8 @@ List<GitCommitEntity> findCommitsForPrByRepoIds(
where c.repository.id IN :repoIds
and (c.authorGithubId = :githubUserId or lower(c.authorEmail) in :emails)
and c.authorName not like '%[bot]%'
and c.authorDate between :from and :to
and c.authorDate >= :from
and c.authorDate < :to
""")
List<CommitDetailProjection> findCommitDetailsByRepoIdsAndIdentity(
@Param("repoIds") List<Long> repoIds,
Expand All @@ -148,7 +154,8 @@ List<CommitDetailProjection> findCommitDetailsByRepoIdsAndIdentity(
from GitCommitEntity c
where c.repository.id IN :repoIds
and c.authorName not like '%[bot]%'
and c.authorDate between :from and :to
and c.authorDate >= :from
and c.authorDate < :to
group by c.repository.id
""")
List<RepoCountProjection> countTotalCommitsByRepoIds(
Expand All @@ -167,7 +174,8 @@ List<RepoCountProjection> countTotalCommitsByRepoIds(
where c.repository.id IN :repoIds
and (c.authorGithubId = :githubUserId or lower(c.authorEmail) in :emails)
and c.authorName not like '%[bot]%'
and c.authorDate between :from and :to
and c.authorDate >= :from
and c.authorDate < :to
group by c.repository.id
""")
List<RepoCountProjection> countCommitsByRepoIdsAndIdentity(
Expand Down Expand Up @@ -228,7 +236,8 @@ List<GitCommitEntity> findPendingCommitsForRepository(
count(c) as recordCount
from GitCommitEntity c
where c.repository.id in :repoIds
and c.authorDate between :from and :to
and c.authorDate >= :from
and c.authorDate < :to
group by c.statsStatus, c.statsSkipReason
""")
List<StatsCoverageProjection> countByStatsStateInRange(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ public interface GitHubPullRequestRepository extends JpaRepository<GitHubPullReq
//
// Matched on the numeric ID: a login is free text, case-sensitive and re-assignable,
// so authorLogin survives as a display value only.
//
// Windows are half-open: `from` inclusive, `to` exclusive, so adjacent windows neither
// double-count a record nor drop one. Callers pass toDate + 1 at midnight UTC.
// -------------------------------------------------------------------------

@Query("""
Expand All @@ -46,7 +49,8 @@ select date(p.createdAt) as day,
from GitHubPullRequestEntity p
where p.repository.id IN :repoIds
and p.authorGithubId = :githubUserId
and p.createdAt between :from and :to
and p.createdAt >= :from
and p.createdAt < :to
group by date(p.createdAt), p.repository.id
order by day, repoId
""")
Expand All @@ -64,7 +68,8 @@ select date(p.mergedAt) as day,
where p.repository.id IN :repoIds
and p.authorGithubId = :githubUserId
and p.merged = true
and p.mergedAt between :from and :to
and p.mergedAt >= :from
and p.mergedAt < :to
group by date(p.mergedAt), p.repository.id
order by day, repoId
""")
Expand All @@ -82,7 +87,8 @@ List<DailyCountProjection> aggregatePrMergedDailyByRepoIdsAndAuthorGithubId(
where p.repository.id IN :repoIds
and p.authorGithubId = :githubUserId
and p.merged = true
and p.mergedAt between :from and :to
and p.mergedAt >= :from
and p.mergedAt < :to
""")
List<PrLeadTimeProjection> findMergedLeadTimesByRepoIdsAndAuthorGithubId(
@Param("repoIds") List<Long> repoIds,
Expand All @@ -96,7 +102,8 @@ List<PrLeadTimeProjection> findMergedLeadTimesByRepoIdsAndAuthorGithubId(
where p.repository.id IN :repoIds
and p.authorGithubId = :githubUserId
and p.merged = true
and p.mergedAt between :from and :to
and p.mergedAt >= :from
and p.mergedAt < :to
""")
List<GitHubPullRequestEntity> findMergedPrsByRepoIdsAndAuthorGithubId(
@Param("repoIds") List<Long> repoIds,
Expand Down Expand Up @@ -156,7 +163,8 @@ List<GitHubPullRequestEntity> findPendingPrsForRepository(
count(p) as recordCount
from GitHubPullRequestEntity p
where p.repository.id in :repoIds
and p.createdAt between :from and :to
and p.createdAt >= :from
and p.createdAt < :to
group by p.statsStatus, p.statsSkipReason
""")
List<StatsCoverageProjection> countByStatsStateInRange(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ public interface IssueRepository extends JpaRepository<IssueEntity, Long> {
// Native SQL: COALESCE(i.repository_id, rm.repository_id) routes GitHub and Jira issues
// through one repo-scoped filter, which JPQL cannot express across nullable FKs. Nullable
// bind parameters are CAST explicitly, since PostgreSQL cannot type a bare NULL.
//
// Windows are half-open: `from` inclusive, `to` exclusive, so adjacent windows neither
// double-count a record nor drop one. Callers pass toDate + 1 at midnight UTC.
// -------------------------------------------------------------------------

@Query(value = """
Expand All @@ -54,7 +57,8 @@ SELECT date(i.created_at) AS day,
WHERE COALESCE(i.repository_id, rm.repository_id) IN (:repoIds)
AND ( (i.source = 'GITHUB' AND i.creator_github_id = CAST(:githubUserId AS bigint))
OR (i.source = 'JIRA' AND i.reporter_account_id = CAST(:jiraAccountId AS text)) )
AND i.created_at BETWEEN :from AND :to
AND i.created_at >= :from
AND i.created_at < :to
GROUP BY date(i.created_at), COALESCE(i.repository_id, rm.repository_id)
ORDER BY day, repoId
""", nativeQuery = true)
Expand All @@ -75,7 +79,8 @@ WHERE COALESCE(i.repository_id, rm.repository_id) IN (:repoIds)
AND ( (i.source = 'GITHUB' AND i.assignee_github_id = CAST(:githubUserId AS bigint))
OR (i.source = 'JIRA' AND i.assignee_account_id = CAST(:jiraAccountId AS text)) )
AND i.closed_at IS NOT NULL
AND i.closed_at BETWEEN :from AND :to
AND i.closed_at >= :from
AND i.closed_at < :to
GROUP BY date(i.closed_at), COALESCE(i.repository_id, rm.repository_id)
ORDER BY day, repoId
""", nativeQuery = true)
Expand All @@ -97,7 +102,8 @@ WHERE COALESCE(i.repository_id, rm.repository_id) IN (:repoIds)
OR (i.source = 'JIRA' AND i.assignee_account_id = CAST(:jiraAccountId AS text)) )
AND i.created_at IS NOT NULL
AND i.closed_at IS NOT NULL
AND i.closed_at BETWEEN :from AND :to
AND i.closed_at >= :from
AND i.closed_at < :to
""", nativeQuery = true)
List<IssueLeadTimeProjection> findIssueLeadTimesByRepoIdsAndIdentity(
@Param("repoIds") List<Long> repoIds,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,47 @@ void aggregateIssuesClosedDailyByRepoIdsAndIdentity_summedOverTeamMembers_equals
assertThat(teamAggregate).isEqualTo(distinctClosedIssuesInRepo());
}

// -------------------------------------------------------------------------
// Window boundary. `to` is exclusive, so an issue stamped exactly on it belongs to the next
// window. One boundary issue — created by A, assigned to B — covers all three queries.
// -------------------------------------------------------------------------

@Test
void aggregateIssuesCreatedDailyByRepoIdsAndIdentity_recordAtWindowEnd_excluded() {
long inside = created(A_GITHUB_ID, A_JIRA);
assertThat(inside).isPositive();

insertGithubIssue("GH-AT-WINDOW-END", A_GITHUB_ID, B_GITHUB_ID, TO, TO);

assertThat(created(A_GITHUB_ID, A_JIRA)).isEqualTo(inside);
}

@Test
void aggregateIssuesClosedDailyByRepoIdsAndIdentity_recordAtWindowEnd_excluded() {
long inside = closed(B_GITHUB_ID, B_JIRA);
assertThat(inside).isPositive();

insertGithubIssue("GH-AT-WINDOW-END", A_GITHUB_ID, B_GITHUB_ID, TO, TO);

assertThat(closed(B_GITHUB_ID, B_JIRA)).isEqualTo(inside);
}

@Test
void findIssueLeadTimesByRepoIdsAndIdentity_recordAtWindowEnd_excluded() {
long inside = leadTimeRows(B_GITHUB_ID, B_JIRA);
assertThat(inside).isPositive();

insertGithubIssue("GH-AT-WINDOW-END", A_GITHUB_ID, B_GITHUB_ID, TO, TO);

assertThat(leadTimeRows(B_GITHUB_ID, B_JIRA)).isEqualTo(inside);
}

/** Lead-time rows the identity matches, one per closed issue. */
private long leadTimeRows(Long githubUserId, String jiraAccountId) {
return issueRepository.findIssueLeadTimesByRepoIdsAndIdentity(
List.of(repoId), githubUserId, jiraAccountId, FROM, TO).size();
}

private long created(Long githubUserId, String jiraAccountId) {
return sum(issueRepository.aggregateIssuesCreatedDailyByRepoIdsAndIdentity(
List.of(repoId), githubUserId, jiraAccountId, FROM, TO));
Expand Down Expand Up @@ -163,12 +204,18 @@ private Long insertRepo(Long dataSourceId, String name) {
}

private void insertGithubIssue(String key, Long creatorGithubId, Long assigneeGithubId) {
insertGithubIssue(key, creatorGithubId, assigneeGithubId, CREATED, CLOSED);
}

/** Overload for the window cases, where created_at and closed_at are the discriminators. */
private void insertGithubIssue(String key, Long creatorGithubId, Long assigneeGithubId,
Instant createdAt, Instant closedAt) {
jdbc.update(
"INSERT INTO issues (repository_id, source_issue_key, title, state, created_at, "
+ "closed_at, creator_github_id, assignee_github_id, source) "
+ "VALUES (?, ?, 'Issue', 'closed', ?, ?, ?, ?, 'GITHUB')",
repoId, key + "-" + System.nanoTime(),
Timestamp.from(CREATED), Timestamp.from(CLOSED), creatorGithubId, assigneeGithubId);
Timestamp.from(createdAt), Timestamp.from(closedAt), creatorGithubId, assigneeGithubId);
}

private Long insertJiraDataSource() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.jdbc.core.JdbcTemplate;

import java.sql.Timestamp;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
Expand All @@ -22,8 +21,8 @@
*
* <p>Attribution compares numeric account ids, so a rename cannot split one person's history and
* differing spellings cannot defeat the self-review exclusion. The timestamp columns are
* {@code TIMESTAMP WITHOUT TIME ZONE}, so fixtures bind UTC {@link LocalDateTime} — see
* {@code EarliestActivityQueryTest}.</p>
* {@code TIMESTAMPTZ}, so fixtures bind an absolute {@link java.sql.Timestamp} rather than a wall
* clock the session zone would reinterpret.</p>
*/
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
Expand Down Expand Up @@ -111,6 +110,76 @@ void countDistinctPrsReviewedByUser_mixOfSelfAndPeerReviews_countsOnlyThePeerRev
.isEqualTo(1);
}

// -------------------------------------------------------------------------
// Window boundary. `to` is exclusive, so a PR stamped exactly on it belongs to the next
// window. The merged queries need their own in-window row: seed() merges nothing.
// -------------------------------------------------------------------------

@Test
void aggregatePrCreatedDailyByRepoIdsAndAuthorGithubId_recordAtWindowEnd_excluded() {
long inside = prsCreated();
assertThat(inside).isPositive();

insertPr(10, STORED_LOGIN, AUTHOR_ID, TO);

assertThat(prsCreated()).isEqualTo(inside);
}

@Test
void aggregatePrMergedDailyByRepoIdsAndAuthorGithubId_recordAtWindowEnd_excluded() {
insertMergedPr(11, AUTHOR_ID, WHEN, WHEN);
long inside = prsMerged();
assertThat(inside).isPositive();

insertMergedPr(12, AUTHOR_ID, WHEN, TO);

assertThat(prsMerged()).isEqualTo(inside);
}

@Test
void findMergedLeadTimesByRepoIdsAndAuthorGithubId_recordAtWindowEnd_excluded() {
insertMergedPr(11, AUTHOR_ID, WHEN, WHEN);
long inside = mergedLeadTimeRows();
assertThat(inside).isPositive();

insertMergedPr(12, AUTHOR_ID, WHEN, TO);

assertThat(mergedLeadTimeRows()).isEqualTo(inside);
}

@Test
void findMergedPrsByRepoIdsAndAuthorGithubId_recordAtWindowEnd_excluded() {
insertMergedPr(11, AUTHOR_ID, WHEN, WHEN);
int inside = prRepository
.findMergedPrsByRepoIdsAndAuthorGithubId(List.of(repoId), AUTHOR_ID, FROM, TO).size();
assertThat(inside).isPositive();

insertMergedPr(12, AUTHOR_ID, WHEN, TO);

assertThat(prRepository.findMergedPrsByRepoIdsAndAuthorGithubId(List.of(repoId), AUTHOR_ID, FROM, TO))
.hasSize(inside);
}

/** PRs the author created inside the window, summed across the per-day rows. */
private long prsCreated() {
return prRepository.aggregatePrCreatedDailyByRepoIdsAndAuthorGithubId(
List.of(repoId), AUTHOR_ID, FROM, TO)
.stream().mapToLong(DailyCountProjection::getCount).sum();
}

/** PRs the author merged inside the window, summed across the per-day rows. */
private long prsMerged() {
return prRepository.aggregatePrMergedDailyByRepoIdsAndAuthorGithubId(
List.of(repoId), AUTHOR_ID, FROM, TO)
.stream().mapToLong(DailyCountProjection::getCount).sum();
}

/** Lead-time rows for the author's merged PRs inside the window. */
private long mergedLeadTimeRows() {
return prRepository.findMergedLeadTimesByRepoIdsAndAuthorGithubId(
List.of(repoId), AUTHOR_ID, FROM, TO).size();
}

// -------------------------------------------------------------------------
// Fixtures
// -------------------------------------------------------------------------
Expand Down Expand Up @@ -139,19 +208,34 @@ private Long insertRepo(Long dataSourceId, String name) {
}

private Long insertPr(int number, String authorLogin, Long authorGithubId) {
return insertPr(number, authorLogin, authorGithubId, WHEN);
}

/** Overload for the window cases, where created_at is the discriminating column. */
private Long insertPr(int number, String authorLogin, Long authorGithubId, Instant createdAt) {
return jdbc.queryForObject(
"INSERT INTO github_pull_requests (repository_id, number, title, author_login, "
+ "author_github_id, state, merged, created_at) "
+ "VALUES (?, ?, 'PR', ?, ?, 'closed', false, ?) RETURNING id",
Long.class, repoId, number, authorLogin, authorGithubId,
LocalDateTime.ofInstant(WHEN, ZoneOffset.UTC));
Timestamp.from(createdAt));
}

/** Merged PRs: the three lead-time and merge queries all filter on merged = true. */
private Long insertMergedPr(int number, Long authorGithubId, Instant createdAt, Instant mergedAt) {
return jdbc.queryForObject(
"INSERT INTO github_pull_requests (repository_id, number, title, author_login, "
+ "author_github_id, state, merged, created_at, merged_at) "
+ "VALUES (?, ?, 'PR', ?, ?, 'closed', true, ?, ?) RETURNING id",
Long.class, repoId, number, STORED_LOGIN, authorGithubId,
Timestamp.from(createdAt), Timestamp.from(mergedAt));
}

private void insertReview(Long prId, String reviewerLogin, Long reviewerGithubId) {
jdbc.update(
"INSERT INTO github_pr_reviews (pr_id, reviewer_login, reviewer_github_id, state, submitted_at) "
+ "VALUES (?, ?, ?, 'APPROVED', ?)",
prId, reviewerLogin, reviewerGithubId,
LocalDateTime.ofInstant(WHEN, ZoneOffset.UTC));
Timestamp.from(WHEN));
}
}
Loading
Loading