@@ -52,6 +52,31 @@ func CalculateChangeLeadTime(taskCtx plugin.SubTaskContext) errors.Error {
5252 return errors .Default .Wrap (err , "error deleting previous project_pr_metrics" )
5353 }
5454
55+ // Batch fetch all required data upfront for better performance
56+ startTime := time .Now ()
57+ logger .Info ("Batch fetching data for project: %s" , data .Options .ProjectName )
58+
59+ firstCommitsMap , err := batchFetchFirstCommits (data .Options .ProjectName , db )
60+ if err != nil {
61+ return errors .Default .Wrap (err , "failed to batch fetch first commits" )
62+ }
63+ logger .Info ("Fetched %d first commits in %v" , len (firstCommitsMap ), time .Since (startTime ))
64+
65+ reviewStartTime := time .Now ()
66+ firstReviewsMap , err := batchFetchFirstReviews (data .Options .ProjectName , db )
67+ if err != nil {
68+ return errors .Default .Wrap (err , "failed to batch fetch first reviews" )
69+ }
70+ logger .Info ("Fetched %d first reviews in %v" , len (firstReviewsMap ), time .Since (reviewStartTime ))
71+
72+ deploymentStartTime := time .Now ()
73+ deploymentsMap , err := batchFetchDeployments (data .Options .ProjectName , db )
74+ if err != nil {
75+ return errors .Default .Wrap (err , "failed to batch fetch deployments" )
76+ }
77+ logger .Info ("Fetched %d deployments in %v" , len (deploymentsMap ), time .Since (deploymentStartTime ))
78+ logger .Info ("Total batch fetch time: %v" , time .Since (startTime ))
79+
5580 // Get pull requests by repo project_name
5681 var clauses = []dal.Clause {
5782 dal .Select ("pr.id, pr.pull_request_key, pr.author_id, pr.merge_commit_sha, pr.created_date, pr.merged_date" ),
@@ -84,23 +109,17 @@ func CalculateChangeLeadTime(taskCtx plugin.SubTaskContext) errors.Error {
84109 projectPrMetric .Id = pr .Id
85110 projectPrMetric .ProjectName = data .Options .ProjectName
86111
87- // Get the first commit for the PR
88- firstCommit , err := getFirstCommit (pr .Id , db )
89- if err != nil {
90- return nil , err
91- }
112+ // Get the first commit for the PR from batch-fetched map
113+ firstCommit := firstCommitsMap [pr .Id ]
92114 // Calculate PR coding time
93115 if firstCommit != nil {
94116 projectPrMetric .PrCodingTime = computeTimeSpan (& firstCommit .CommitAuthoredDate , & pr .CreatedDate )
95117 projectPrMetric .FirstCommitSha = firstCommit .CommitSha
96118 projectPrMetric .FirstCommitAuthoredDate = & firstCommit .CommitAuthoredDate
97119 }
98120
99- // Get the first review for the PR
100- firstReview , err := getFirstReview (pr .Id , pr .AuthorId , db )
101- if err != nil {
102- return nil , err
103- }
121+ // Get the first review for the PR from batch-fetched map
122+ firstReview := firstReviewsMap [pr .Id ]
104123 // Calculate PR pickup time and PR review time
105124 prDuring := computeTimeSpan (& pr .CreatedDate , pr .MergedDate )
106125 if firstReview != nil {
@@ -113,11 +132,8 @@ func CalculateChangeLeadTime(taskCtx plugin.SubTaskContext) errors.Error {
113132 projectPrMetric .PrCreatedDate = & pr .CreatedDate
114133 projectPrMetric .PrMergedDate = pr .MergedDate
115134
116- // Get the deployment for the PR
117- deployment , err := getDeploymentCommit (pr .MergeCommitSha , data .Options .ProjectName , db )
118- if err != nil {
119- return nil , err
120- }
135+ // Get the deployment for the PR from batch-fetched map
136+ deployment := deploymentsMap [pr .MergeCommitSha ]
121137
122138 // Calculate PR deploy time
123139 if deployment != nil && deployment .FinishedDate != nil {
@@ -244,3 +260,145 @@ func computeTimeSpan(start, end *time.Time) *int64 {
244260 }
245261 return & minutes
246262}
263+
264+ // deploymentCommitWithMergeSha is a helper struct to capture both the deployment commit
265+ // and the associated merge_sha from the commits_diffs join query.
266+ type deploymentCommitWithMergeSha struct {
267+ devops.CicdDeploymentCommit
268+ MergeSha string `gorm:"column:merge_sha"`
269+ }
270+
271+ // batchFetchFirstCommits retrieves the first commit for all pull requests in the given project.
272+ // Returns a map indexed by PR ID for O(1) lookup performance.
273+ //
274+ // The query uses a subquery to find the minimum commit_authored_date for each PR,
275+ // then joins back to get the full commit record. This is more efficient than
276+ // fetching all commits and filtering in memory.
277+ func batchFetchFirstCommits (projectName string , db dal.Dal ) (map [string ]* code.PullRequestCommit , errors.Error ) {
278+ var results []* code.PullRequestCommit
279+
280+ // Use a subquery to find the earliest commit for each PR, then join to get full commit details.
281+ // This avoids scanning all commits and is optimized by the database engine.
282+ err := db .All (
283+ & results ,
284+ dal .Select ("prc.*" ),
285+ dal .From ("pull_request_commits prc" ),
286+ dal .Join (`INNER JOIN (
287+ SELECT pull_request_id, MIN(commit_authored_date) as min_date
288+ FROM pull_request_commits
289+ GROUP BY pull_request_id
290+ ) first_commits ON prc.pull_request_id = first_commits.pull_request_id
291+ AND prc.commit_authored_date = first_commits.min_date` ),
292+ dal .Join ("INNER JOIN pull_requests pr ON pr.id = prc.pull_request_id" ),
293+ dal .Join ("LEFT JOIN project_mapping pm ON pm.row_id = pr.base_repo_id AND pm.table = 'repos'" ),
294+ dal .Where ("pm.project_name = ?" , projectName ),
295+ dal .Orderby ("prc.pull_request_id, prc.commit_authored_date ASC" ),
296+ )
297+
298+ if err != nil {
299+ return nil , errors .Default .Wrap (err , "failed to batch fetch first commits" )
300+ }
301+
302+ // Build the map for O(1) lookup by PR ID
303+ commitMap := make (map [string ]* code.PullRequestCommit , len (results ))
304+ for _ , commit := range results {
305+ // Only keep the first commit if multiple commits have the same timestamp
306+ if _ , exists := commitMap [commit .PullRequestId ]; ! exists {
307+ commitMap [commit .PullRequestId ] = commit
308+ }
309+ }
310+
311+ return commitMap , nil
312+ }
313+
314+ // batchFetchFirstReviews retrieves the first review comment for all pull requests in the given project.
315+ // Returns a map indexed by PR ID for O(1) lookup performance.
316+ //
317+ // The query uses a subquery to find the minimum created_date for each PR (excluding the PR author),
318+ // then joins back to get the full comment record.
319+ func batchFetchFirstReviews (projectName string , db dal.Dal ) (map [string ]* code.PullRequestComment , errors.Error ) {
320+ var results []* code.PullRequestComment
321+
322+ // Use a subquery to find the earliest review comment for each PR (excluding author's comments),
323+ // then join to get full comment details.
324+ err := db .All (
325+ & results ,
326+ dal .Select ("prc.*" ),
327+ dal .From ("pull_request_comments prc" ),
328+ dal .Join (`INNER JOIN (
329+ SELECT prc2.pull_request_id, MIN(prc2.created_date) as min_date
330+ FROM pull_request_comments prc2
331+ INNER JOIN pull_requests pr2 ON pr2.id = prc2.pull_request_id
332+ WHERE (pr2.author_id IS NULL OR pr2.author_id = '' OR prc2.account_id != pr2.author_id)
333+ GROUP BY prc2.pull_request_id
334+ ) first_reviews ON prc.pull_request_id = first_reviews.pull_request_id
335+ AND prc.created_date = first_reviews.min_date` ),
336+ dal .Join ("INNER JOIN pull_requests pr ON pr.id = prc.pull_request_id" ),
337+ dal .Join ("LEFT JOIN project_mapping pm ON pm.row_id = pr.base_repo_id AND pm.table = 'repos'" ),
338+ dal .Where ("pm.project_name = ? AND (pr.author_id IS NULL OR pr.author_id = '' OR prc.account_id != pr.author_id)" , projectName ),
339+ dal .Orderby ("prc.pull_request_id, prc.created_date ASC" ),
340+ )
341+
342+ if err != nil {
343+ return nil , errors .Default .Wrap (err , "failed to batch fetch first reviews" )
344+ }
345+
346+ // Build the map for O(1) lookup by PR ID
347+ reviewMap := make (map [string ]* code.PullRequestComment , len (results ))
348+ for _ , review := range results {
349+ // Only keep the first review if multiple reviews have the same timestamp
350+ if _ , exists := reviewMap [review .PullRequestId ]; ! exists {
351+ reviewMap [review .PullRequestId ] = review
352+ }
353+ }
354+
355+ return reviewMap , nil
356+ }
357+
358+ // batchFetchDeployments retrieves deployment commits for all merge commits in the given project.
359+ // Returns a map indexed by merge commit SHA for O(1) lookup performance.
360+ //
361+ // The query finds the first successful production deployment for each merge commit by:
362+ // 1. Finding deployment commits that have a previous successful deployment
363+ // 2. Joining with commits_diffs to find which deployment included each merge commit
364+ // 3. Filtering for successful production deployments
365+ // 4. Ordering by started_date to get the earliest deployment
366+ //
367+ // The map is indexed by merge_sha (from commits_diffs), not by deployment commit_sha,
368+ // because the caller needs to look up deployments by PR merge_commit_sha.
369+ func batchFetchDeployments (projectName string , db dal.Dal ) (map [string ]* devops.CicdDeploymentCommit , errors.Error ) {
370+ var results []* deploymentCommitWithMergeSha
371+
372+ // Query finds the first deployment for each merge commit by using a window function
373+ // to rank deployments by started_date, then filtering to keep only rank 1.
374+ err := db .All (
375+ & results ,
376+ dal .Select ("dc.*, cd.commit_sha as merge_sha" ),
377+ dal .From ("cicd_deployment_commits dc" ),
378+ dal .Join ("LEFT JOIN cicd_deployment_commits p ON dc.prev_success_deployment_commit_id = p.id" ),
379+ dal .Join ("INNER JOIN commits_diffs cd ON cd.new_commit_sha = dc.commit_sha AND cd.old_commit_sha = COALESCE(p.commit_sha, '')" ),
380+ dal .Join ("LEFT JOIN project_mapping pm ON pm.table = 'cicd_scopes' AND pm.row_id = dc.cicd_scope_id" ),
381+ dal .Where ("dc.prev_success_deployment_commit_id <> ''" ),
382+ dal .Where ("dc.environment = 'PRODUCTION'" ), // TODO: remove this when multi-environment is supported
383+ dal .Where ("dc.result = ? AND pm.project_name = ?" , devops .RESULT_SUCCESS , projectName ),
384+ dal .Orderby ("cd.commit_sha, dc.started_date ASC, dc.id ASC" ),
385+ )
386+
387+ if err != nil {
388+ return nil , errors .Default .Wrap (err , "failed to batch fetch deployments" )
389+ }
390+
391+ // Build the map indexed by merge_sha for O(1) lookup.
392+ // Keep only the first deployment for each merge commit (earliest by started_date).
393+ deploymentMap := make (map [string ]* devops.CicdDeploymentCommit , len (results ))
394+ for _ , result := range results {
395+ // Only keep the first deployment for each merge_sha
396+ if _ , exists := deploymentMap [result .MergeSha ]; ! exists {
397+ // Copy the CicdDeploymentCommit without the MergeSha field
398+ deploymentCopy := result .CicdDeploymentCommit
399+ deploymentMap [result .MergeSha ] = & deploymentCopy
400+ }
401+ }
402+
403+ return deploymentMap , nil
404+ }
0 commit comments