mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-08 01:16:45 +08:00
Before: a project had no way to list the pull requests its issues delivered; the only PR list was per issue, so a project-level view would have to fan out one request per issue and could not page or count them.
After: GET /api/projects/{id}/pull-requests returns the distinct GitHub and self-hosted VCS PRs linked to the project's issues, newest update first, with the issue PR list's fields (checks rollup, mergeable, additions, deletions) plus the project issues each PR is linked to. state=open|merged|closed filters (open covers draft, closed is closed without merging), limit/offset page per PR, and the response carries total and per-group counts.
How: ListProjectPullRequestPage unions both PR tables and de-duplicates links so paging is per PR; ListProjectPullRequests / ListProjectVCSPullRequests then load full rows for the page ids with the same columns as the per-issue queries, so the existing row serializers are reused unchanged (including the page-visit snapshot refresh). Issues are attached from one extra query. CountProjectPullRequestsByState feeds total and counts. No migration: additions/deletions already exist. DB-backed handler tests are written but not run here (no PostgreSQL).
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_017deksZDSzZBA4QSqZZoBip
471 lines
16 KiB
Go
471 lines
16 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.31.1
|
|
// source: project_pull_request.sql
|
|
|
|
package db
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
const countProjectPullRequestsByState = `-- name: CountProjectPullRequestsByState :many
|
|
WITH linked AS (
|
|
SELECT DISTINCT pr.id, pr.state
|
|
FROM issue_pull_request ipr
|
|
JOIN github_pull_request pr ON pr.id = ipr.pull_request_id
|
|
JOIN issue i ON i.id = ipr.issue_id
|
|
WHERE i.project_id = $1
|
|
AND i.workspace_id = $2
|
|
AND pr.workspace_id = $2
|
|
UNION ALL
|
|
SELECT DISTINCT pr.id, pr.state
|
|
FROM issue_vcs_pull_request ipr
|
|
JOIN vcs_pull_request pr ON pr.id = ipr.pull_request_id
|
|
JOIN issue i ON i.id = ipr.issue_id
|
|
WHERE i.project_id = $1
|
|
AND i.workspace_id = $2
|
|
AND pr.workspace_id = $2
|
|
)
|
|
SELECT state, COUNT(*)::bigint AS total
|
|
FROM linked
|
|
GROUP BY state
|
|
`
|
|
|
|
type CountProjectPullRequestsByStateParams struct {
|
|
ProjectID pgtype.UUID `json:"project_id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
type CountProjectPullRequestsByStateRow struct {
|
|
State string `json:"state"`
|
|
Total int64 `json:"total"`
|
|
}
|
|
|
|
// Distinct linked PR count per stored state, over GitHub and VCS PRs. Ignores
|
|
// any state filter so the tab can show every group's count from one request.
|
|
func (q *Queries) CountProjectPullRequestsByState(ctx context.Context, arg CountProjectPullRequestsByStateParams) ([]CountProjectPullRequestsByStateRow, error) {
|
|
rows, err := q.db.Query(ctx, countProjectPullRequestsByState, arg.ProjectID, arg.WorkspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []CountProjectPullRequestsByStateRow{}
|
|
for rows.Next() {
|
|
var i CountProjectPullRequestsByStateRow
|
|
if err := rows.Scan(&i.State, &i.Total); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listProjectPullRequestIssues = `-- name: ListProjectPullRequestIssues :many
|
|
SELECT 'github'::text AS kind, ipr.pull_request_id AS pr_id, i.id AS issue_id, i.number, i.title
|
|
FROM issue_pull_request ipr
|
|
JOIN issue i ON i.id = ipr.issue_id
|
|
WHERE ipr.pull_request_id = ANY($1::uuid[])
|
|
AND i.project_id = $2
|
|
AND i.workspace_id = $3
|
|
UNION ALL
|
|
SELECT pr.provider AS kind, ipr.pull_request_id AS pr_id, i.id AS issue_id, i.number, i.title
|
|
FROM issue_vcs_pull_request ipr
|
|
JOIN vcs_pull_request pr ON pr.id = ipr.pull_request_id
|
|
JOIN issue i ON i.id = ipr.issue_id
|
|
WHERE ipr.pull_request_id = ANY($4::uuid[])
|
|
AND i.project_id = $2
|
|
AND i.workspace_id = $3
|
|
ORDER BY number
|
|
`
|
|
|
|
type ListProjectPullRequestIssuesParams struct {
|
|
GithubPrIds []pgtype.UUID `json:"github_pr_ids"`
|
|
ProjectID pgtype.UUID `json:"project_id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
VcsPrIds []pgtype.UUID `json:"vcs_pr_ids"`
|
|
}
|
|
|
|
type ListProjectPullRequestIssuesRow struct {
|
|
Kind string `json:"kind"`
|
|
PrID pgtype.UUID `json:"pr_id"`
|
|
IssueID pgtype.UUID `json:"issue_id"`
|
|
Number int32 `json:"number"`
|
|
Title string `json:"title"`
|
|
}
|
|
|
|
// The project's issues linked to each of the given PRs, GitHub and VCS ids
|
|
// passed separately; kind is "github" or the VCS provider.
|
|
func (q *Queries) ListProjectPullRequestIssues(ctx context.Context, arg ListProjectPullRequestIssuesParams) ([]ListProjectPullRequestIssuesRow, error) {
|
|
rows, err := q.db.Query(ctx, listProjectPullRequestIssues,
|
|
arg.GithubPrIds,
|
|
arg.ProjectID,
|
|
arg.WorkspaceID,
|
|
arg.VcsPrIds,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListProjectPullRequestIssuesRow{}
|
|
for rows.Next() {
|
|
var i ListProjectPullRequestIssuesRow
|
|
if err := rows.Scan(
|
|
&i.Kind,
|
|
&i.PrID,
|
|
&i.IssueID,
|
|
&i.Number,
|
|
&i.Title,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listProjectPullRequestPage = `-- name: ListProjectPullRequestPage :many
|
|
WITH linked AS (
|
|
SELECT DISTINCT 'github'::text AS kind, pr.id, pr.state, pr.pr_updated_at
|
|
FROM issue_pull_request ipr
|
|
JOIN github_pull_request pr ON pr.id = ipr.pull_request_id
|
|
JOIN issue i ON i.id = ipr.issue_id
|
|
WHERE i.project_id = $4
|
|
AND i.workspace_id = $5
|
|
AND pr.workspace_id = $5
|
|
UNION ALL
|
|
SELECT DISTINCT pr.provider AS kind, pr.id, pr.state, pr.pr_updated_at
|
|
FROM issue_vcs_pull_request ipr
|
|
JOIN vcs_pull_request pr ON pr.id = ipr.pull_request_id
|
|
JOIN issue i ON i.id = ipr.issue_id
|
|
WHERE i.project_id = $4
|
|
AND i.workspace_id = $5
|
|
AND pr.workspace_id = $5
|
|
)
|
|
SELECT kind, id
|
|
FROM linked
|
|
WHERE cardinality($1::text[]) = 0
|
|
OR state = ANY($1::text[])
|
|
ORDER BY pr_updated_at DESC, id DESC
|
|
LIMIT $3 OFFSET $2
|
|
`
|
|
|
|
type ListProjectPullRequestPageParams struct {
|
|
StateFilter []string `json:"state_filter"`
|
|
PageOffset int32 `json:"page_offset"`
|
|
PageLimit int32 `json:"page_limit"`
|
|
ProjectID pgtype.UUID `json:"project_id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
type ListProjectPullRequestPageRow struct {
|
|
Kind string `json:"kind"`
|
|
ID pgtype.UUID `json:"id"`
|
|
}
|
|
|
|
// One page of the distinct PRs (GitHub and self-hosted VCS together) linked to
|
|
// any issue of a project, newest update first. A PR linked to several issues of
|
|
// the project is one row, so limit/offset page per PR rather than per link.
|
|
// state_filter is the set of stored states to keep; empty keeps every state.
|
|
func (q *Queries) ListProjectPullRequestPage(ctx context.Context, arg ListProjectPullRequestPageParams) ([]ListProjectPullRequestPageRow, error) {
|
|
rows, err := q.db.Query(ctx, listProjectPullRequestPage,
|
|
arg.StateFilter,
|
|
arg.PageOffset,
|
|
arg.PageLimit,
|
|
arg.ProjectID,
|
|
arg.WorkspaceID,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListProjectPullRequestPageRow{}
|
|
for rows.Next() {
|
|
var i ListProjectPullRequestPageRow
|
|
if err := rows.Scan(&i.Kind, &i.ID); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listProjectPullRequests = `-- name: ListProjectPullRequests :many
|
|
WITH target_prs AS (
|
|
SELECT pr.id, pr.snapshot_head_sha
|
|
FROM github_pull_request pr
|
|
WHERE pr.id = ANY($1::uuid[])
|
|
AND pr.workspace_id = $2
|
|
),
|
|
checks AS (
|
|
SELECT
|
|
cr.pr_id,
|
|
COUNT(*)::bigint AS total,
|
|
SUM(CASE WHEN cr.status = 'completed' AND cr.conclusion IN
|
|
('failure','cancelled','timed_out','action_required','startup_failure','stale','error')
|
|
THEN 1 ELSE 0 END)::bigint AS failed,
|
|
SUM(CASE WHEN cr.status = 'completed' AND cr.conclusion IN
|
|
('success','neutral','skipped')
|
|
THEN 1 ELSE 0 END)::bigint AS passed,
|
|
SUM(CASE WHEN cr.status <> 'completed' OR cr.conclusion IS NULL
|
|
THEN 1 ELSE 0 END)::bigint AS running,
|
|
COALESCE(
|
|
array_agg(cr.name) FILTER (WHERE cr.status = 'completed' AND cr.conclusion IN
|
|
('failure','cancelled','timed_out','action_required','startup_failure','stale','error')),
|
|
'{}'
|
|
)::text[] AS failed_names
|
|
FROM github_pull_request_check_run cr
|
|
JOIN target_prs tp ON tp.id = cr.pr_id
|
|
WHERE cr.head_sha = tp.snapshot_head_sha AND tp.snapshot_head_sha <> ''
|
|
GROUP BY cr.pr_id
|
|
)
|
|
SELECT
|
|
pr.id, pr.workspace_id, pr.installation_id, pr.repo_owner, pr.repo_name,
|
|
pr.pr_number, pr.title, pr.state, pr.html_url, pr.branch, pr.author_login,
|
|
pr.author_avatar_url, pr.merged_at, pr.closed_at, pr.pr_created_at,
|
|
pr.pr_updated_at, pr.head_sha, pr.mergeable_state,
|
|
pr.additions, pr.deletions, pr.changed_files,
|
|
pr.api_mergeable, pr.api_merge_state_status, pr.checks_rollup_state,
|
|
pr.snapshot_head_sha, pr.snapshot_fetched_at,
|
|
pr.created_at, pr.updated_at,
|
|
'system'::text AS linked_by_type,
|
|
COALESCE(c.total, 0)::bigint AS checks_total,
|
|
COALESCE(c.passed, 0)::bigint AS checks_passed,
|
|
COALESCE(c.failed, 0)::bigint AS checks_failed,
|
|
COALESCE(c.running, 0)::bigint AS checks_running,
|
|
COALESCE(c.failed_names, '{}')::text[] AS failed_check_names
|
|
FROM github_pull_request pr
|
|
JOIN target_prs tp ON tp.id = pr.id
|
|
LEFT JOIN checks c ON c.pr_id = pr.id
|
|
`
|
|
|
|
type ListProjectPullRequestsParams struct {
|
|
PrIds []pgtype.UUID `json:"pr_ids"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
type ListProjectPullRequestsRow struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
InstallationID int64 `json:"installation_id"`
|
|
RepoOwner string `json:"repo_owner"`
|
|
RepoName string `json:"repo_name"`
|
|
PrNumber int32 `json:"pr_number"`
|
|
Title string `json:"title"`
|
|
State string `json:"state"`
|
|
HtmlUrl string `json:"html_url"`
|
|
Branch pgtype.Text `json:"branch"`
|
|
AuthorLogin pgtype.Text `json:"author_login"`
|
|
AuthorAvatarUrl pgtype.Text `json:"author_avatar_url"`
|
|
MergedAt pgtype.Timestamptz `json:"merged_at"`
|
|
ClosedAt pgtype.Timestamptz `json:"closed_at"`
|
|
PrCreatedAt pgtype.Timestamptz `json:"pr_created_at"`
|
|
PrUpdatedAt pgtype.Timestamptz `json:"pr_updated_at"`
|
|
HeadSha string `json:"head_sha"`
|
|
MergeableState pgtype.Text `json:"mergeable_state"`
|
|
Additions int32 `json:"additions"`
|
|
Deletions int32 `json:"deletions"`
|
|
ChangedFiles int32 `json:"changed_files"`
|
|
ApiMergeable pgtype.Text `json:"api_mergeable"`
|
|
ApiMergeStateStatus pgtype.Text `json:"api_merge_state_status"`
|
|
ChecksRollupState pgtype.Text `json:"checks_rollup_state"`
|
|
SnapshotHeadSha string `json:"snapshot_head_sha"`
|
|
SnapshotFetchedAt pgtype.Timestamptz `json:"snapshot_fetched_at"`
|
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
LinkedByType string `json:"linked_by_type"`
|
|
ChecksTotal int64 `json:"checks_total"`
|
|
ChecksPassed int64 `json:"checks_passed"`
|
|
ChecksFailed int64 `json:"checks_failed"`
|
|
ChecksRunning int64 `json:"checks_running"`
|
|
FailedCheckNames []string `json:"failed_check_names"`
|
|
}
|
|
|
|
// GitHub PR rows for ids picked by ListProjectPullRequestPage, with the same
|
|
// columns and snapshot / check-run aggregation as ListPullRequestsByIssue (the
|
|
// handler reuses its row serializer). There is no per-issue link here, so
|
|
// linked_by_type is a constant.
|
|
func (q *Queries) ListProjectPullRequests(ctx context.Context, arg ListProjectPullRequestsParams) ([]ListProjectPullRequestsRow, error) {
|
|
rows, err := q.db.Query(ctx, listProjectPullRequests, arg.PrIds, arg.WorkspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListProjectPullRequestsRow{}
|
|
for rows.Next() {
|
|
var i ListProjectPullRequestsRow
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.InstallationID,
|
|
&i.RepoOwner,
|
|
&i.RepoName,
|
|
&i.PrNumber,
|
|
&i.Title,
|
|
&i.State,
|
|
&i.HtmlUrl,
|
|
&i.Branch,
|
|
&i.AuthorLogin,
|
|
&i.AuthorAvatarUrl,
|
|
&i.MergedAt,
|
|
&i.ClosedAt,
|
|
&i.PrCreatedAt,
|
|
&i.PrUpdatedAt,
|
|
&i.HeadSha,
|
|
&i.MergeableState,
|
|
&i.Additions,
|
|
&i.Deletions,
|
|
&i.ChangedFiles,
|
|
&i.ApiMergeable,
|
|
&i.ApiMergeStateStatus,
|
|
&i.ChecksRollupState,
|
|
&i.SnapshotHeadSha,
|
|
&i.SnapshotFetchedAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.LinkedByType,
|
|
&i.ChecksTotal,
|
|
&i.ChecksPassed,
|
|
&i.ChecksFailed,
|
|
&i.ChecksRunning,
|
|
&i.FailedCheckNames,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listProjectVCSPullRequests = `-- name: ListProjectVCSPullRequests :many
|
|
WITH checks AS (
|
|
SELECT
|
|
pr.id AS pr_id,
|
|
COUNT(*)::bigint AS total,
|
|
SUM(CASE WHEN cs.state = 'failed' THEN 1 ELSE 0 END)::bigint AS failed,
|
|
SUM(CASE WHEN cs.state = 'passed' THEN 1 ELSE 0 END)::bigint AS passed,
|
|
SUM(CASE WHEN cs.state = 'pending' THEN 1 ELSE 0 END)::bigint AS pending
|
|
FROM vcs_pull_request pr
|
|
JOIN vcs_commit_status cs
|
|
ON cs.connection_id = pr.connection_id
|
|
AND cs.sha = pr.head_sha
|
|
AND pr.head_sha <> ''
|
|
WHERE pr.id = ANY($1::uuid[])
|
|
AND pr.workspace_id = $2
|
|
GROUP BY pr.id
|
|
)
|
|
SELECT
|
|
pr.id, pr.workspace_id, pr.connection_id, pr.provider, pr.repo_owner, pr.repo_name, pr.pr_number, pr.title, pr.state, pr.html_url, pr.branch, pr.head_sha, pr.author_login, pr.author_avatar_url, pr.merged_at, pr.closed_at, pr.pr_created_at, pr.pr_updated_at, pr.additions, pr.deletions, pr.changed_files, pr.created_at, pr.updated_at,
|
|
'system'::text AS linked_by_type,
|
|
COALESCE(c.total, 0)::bigint AS checks_total,
|
|
COALESCE(c.passed, 0)::bigint AS checks_passed,
|
|
COALESCE(c.failed, 0)::bigint AS checks_failed,
|
|
COALESCE(c.pending, 0)::bigint AS checks_pending
|
|
FROM vcs_pull_request pr
|
|
LEFT JOIN checks c ON c.pr_id = pr.id
|
|
WHERE pr.id = ANY($1::uuid[])
|
|
AND pr.workspace_id = $2
|
|
`
|
|
|
|
type ListProjectVCSPullRequestsParams struct {
|
|
PrIds []pgtype.UUID `json:"pr_ids"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
type ListProjectVCSPullRequestsRow struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
ConnectionID pgtype.UUID `json:"connection_id"`
|
|
Provider string `json:"provider"`
|
|
RepoOwner string `json:"repo_owner"`
|
|
RepoName string `json:"repo_name"`
|
|
PrNumber int32 `json:"pr_number"`
|
|
Title string `json:"title"`
|
|
State string `json:"state"`
|
|
HtmlUrl string `json:"html_url"`
|
|
Branch pgtype.Text `json:"branch"`
|
|
HeadSha string `json:"head_sha"`
|
|
AuthorLogin pgtype.Text `json:"author_login"`
|
|
AuthorAvatarUrl pgtype.Text `json:"author_avatar_url"`
|
|
MergedAt pgtype.Timestamptz `json:"merged_at"`
|
|
ClosedAt pgtype.Timestamptz `json:"closed_at"`
|
|
PrCreatedAt pgtype.Timestamptz `json:"pr_created_at"`
|
|
PrUpdatedAt pgtype.Timestamptz `json:"pr_updated_at"`
|
|
Additions int32 `json:"additions"`
|
|
Deletions int32 `json:"deletions"`
|
|
ChangedFiles int32 `json:"changed_files"`
|
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
LinkedByType string `json:"linked_by_type"`
|
|
ChecksTotal int64 `json:"checks_total"`
|
|
ChecksPassed int64 `json:"checks_passed"`
|
|
ChecksFailed int64 `json:"checks_failed"`
|
|
ChecksPending int64 `json:"checks_pending"`
|
|
}
|
|
|
|
// Self-hosted VCS PR rows for ids picked by ListProjectPullRequestPage, shaped
|
|
// like ListVCSPullRequestsByIssue (commit statuses of the current head sha
|
|
// folded into passed / failed / pending counts).
|
|
func (q *Queries) ListProjectVCSPullRequests(ctx context.Context, arg ListProjectVCSPullRequestsParams) ([]ListProjectVCSPullRequestsRow, error) {
|
|
rows, err := q.db.Query(ctx, listProjectVCSPullRequests, arg.PrIds, arg.WorkspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListProjectVCSPullRequestsRow{}
|
|
for rows.Next() {
|
|
var i ListProjectVCSPullRequestsRow
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.ConnectionID,
|
|
&i.Provider,
|
|
&i.RepoOwner,
|
|
&i.RepoName,
|
|
&i.PrNumber,
|
|
&i.Title,
|
|
&i.State,
|
|
&i.HtmlUrl,
|
|
&i.Branch,
|
|
&i.HeadSha,
|
|
&i.AuthorLogin,
|
|
&i.AuthorAvatarUrl,
|
|
&i.MergedAt,
|
|
&i.ClosedAt,
|
|
&i.PrCreatedAt,
|
|
&i.PrUpdatedAt,
|
|
&i.Additions,
|
|
&i.Deletions,
|
|
&i.ChangedFiles,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.LinkedByType,
|
|
&i.ChecksTotal,
|
|
&i.ChecksPassed,
|
|
&i.ChecksFailed,
|
|
&i.ChecksPending,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|