fix: drop N+1 db query on template ACL available (#25465)

Fixes
[PLAT-149](https://linear.app/codercom/issue/PLAT-149/template-permissions-search-is-extremely-slow-with-many-groups).

`/acl/available` ran a db query per group. A deployment with >5,000
groups made this route extremely slow.
This commit is contained in:
Steven Masley
2026-05-20 22:40:50 +00:00
committed by GitHub
parent 889add734e
commit 9b6eadab77
12 changed files with 315 additions and 26 deletions
+63
View File
@@ -12689,6 +12689,56 @@ func (q *sqlQuerier) GetGroupMembersCountByGroupID(ctx context.Context, arg GetG
return count, err
}
const getGroupMembersCountByGroupIDs = `-- name: GetGroupMembersCountByGroupIDs :many
SELECT
group_id,
COUNT(*) AS member_count
FROM group_members_expanded
WHERE group_id = ANY($1 :: uuid[])
AND CASE
WHEN $2::bool THEN TRUE
ELSE user_is_system = false
END
GROUP BY group_id
`
type GetGroupMembersCountByGroupIDsParams struct {
GroupIds []uuid.UUID `db:"group_ids" json:"group_ids"`
IncludeSystem bool `db:"include_system" json:"include_system"`
}
type GetGroupMembersCountByGroupIDsRow struct {
GroupID uuid.UUID `db:"group_id" json:"group_id"`
MemberCount int64 `db:"member_count" json:"member_count"`
}
// Returns the total member count for each of the given group IDs in a
// single query. Used to avoid N+1 lookups when listing many groups. Like
// GetGroupMembersCountByGroupID, the count is returned even when the
// caller does not have read access to individual group members.
func (q *sqlQuerier) GetGroupMembersCountByGroupIDs(ctx context.Context, arg GetGroupMembersCountByGroupIDsParams) ([]GetGroupMembersCountByGroupIDsRow, error) {
rows, err := q.db.QueryContext(ctx, getGroupMembersCountByGroupIDs, pq.Array(arg.GroupIds), arg.IncludeSystem)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetGroupMembersCountByGroupIDsRow
for rows.Next() {
var i GetGroupMembersCountByGroupIDsRow
if err := rows.Scan(&i.GroupID, &i.MemberCount); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertGroupMember = `-- name: InsertGroupMember :exec
INSERT INTO
group_members (user_id, group_id)
@@ -12906,6 +12956,14 @@ WHERE
groups.id = ANY($4)
ELSE true
END
-- Filter by group name or display name (substring, case-insensitive).
AND CASE WHEN $5 :: text != '' THEN (
groups.name ILIKE concat('%', $5, '%')
OR groups.display_name ILIKE concat('%', $5, '%')
)
ELSE true
END
LIMIT NULLIF($6 :: int, 0)
`
type GetGroupsParams struct {
@@ -12913,6 +12971,8 @@ type GetGroupsParams struct {
HasMemberID uuid.UUID `db:"has_member_id" json:"has_member_id"`
GroupNames []string `db:"group_names" json:"group_names"`
GroupIds []uuid.UUID `db:"group_ids" json:"group_ids"`
Search string `db:"search" json:"search"`
LimitOpt int32 `db:"limit_opt" json:"limit_opt"`
}
type GetGroupsRow struct {
@@ -12921,12 +12981,15 @@ type GetGroupsRow struct {
OrganizationDisplayName string `db:"organization_display_name" json:"organization_display_name"`
}
// A limit of 0 means "no limit".
func (q *sqlQuerier) GetGroups(ctx context.Context, arg GetGroupsParams) ([]GetGroupsRow, error) {
rows, err := q.db.QueryContext(ctx, getGroups,
arg.OrganizationID,
arg.HasMemberID,
pq.Array(arg.GroupNames),
pq.Array(arg.GroupIds),
arg.Search,
arg.LimitOpt,
)
if err != nil {
return nil, err