Skip to content

Commit b0f042b

Browse files
authored
Merge pull request #167 from unisoncomputing/cp/faster-project-search
Speed up project permission queries
2 parents 7f238d1 + 6a099bf commit b0f042b

File tree

6 files changed

+68
-29
lines changed

6 files changed

+68
-29
lines changed

share-api/src/Share/Postgres/Contributions/Queries.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,10 +260,10 @@ listContributionsByUserId callerUserId userId limit mayCursor mayStatusFilter ma
260260
contribution.author_id,
261261
(SELECT COUNT(*) FROM comments comment WHERE comment.contribution_id = contribution.id AND comment.deleted_at IS NULL) as num_comments
262262
FROM contributions AS contribution
263-
JOIN projects AS project ON project.id = contribution.project_id
263+
JOIN projects_by_user_permission(#{callerUserId}, #{ProjectView}) AS project
264+
ON project.id = contribution.project_id
264265
WHERE
265266
contribution.author_id = #{userId}
266-
AND user_has_project_permission(#{callerUserId}, project.id, #{ProjectView})
267267
AND (#{mayStatusFilter} IS NULL OR contribution.status = #{mayStatusFilter})
268268
AND ^{cursorFilter}
269269
AND ^{kindFilter}

share-api/src/Share/Postgres/Queries.hs

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -156,22 +156,20 @@ searchProjects caller userIdFilter (Query query) psk limit = do
156156
PG.queryListRows @(Project PG.:. PG.Only UserHandle)
157157
[PG.sql|
158158
SELECT p.id, p.owner_user_id, p.slug, p.summary, p.tags, p.private, p.created_at, p.updated_at, owner.handle
159-
FROM projects p
159+
FROM projects_by_user_permission(#{caller}, #{ProjectView}) p
160160
JOIN users owner ON p.owner_user_id = owner.id
161161
WHERE p.owner_user_id = #{userId}
162-
AND user_has_project_permission(#{caller}, p.id, #{ProjectView})
163-
ORDER BY p.created_at DESC
162+
ORDER BY p.created_at DESC, p.slug ASC
164163
LIMIT #{limit}
165164
|]
166165
_ -> do
167166
PG.queryListRows
168167
[PG.sql|
169168
SELECT p.id, p.owner_user_id, p.slug, p.summary, p.tags, p.private, p.created_at, p.updated_at, owner.handle
170-
FROM websearch_to_tsquery('english', #{query}) AS tokenquery, projects AS p
169+
FROM websearch_to_tsquery('english', #{query}) AS tokenquery, projects_by_user_permission(#{caller}, #{ProjectView}) AS p
171170
JOIN users AS owner ON p.owner_user_id = owner.id
172171
WHERE (tokenquery @@ p.project_text_document OR p.slug ILIKE ('%' || like_escape(#{query}) || '%'))
173172
AND (#{userIdFilter} IS NULL OR p.owner_user_id = #{userIdFilter})
174-
AND user_has_project_permission(#{caller}, p.id, #{ProjectView})
175173
^{pskFilter}
176174
ORDER BY
177175
p.slug = #{query} DESC,
@@ -275,11 +273,10 @@ listProjectsByUserWithMetadata callerUserId projectOwnerUserId = do
275273
owner.handle,
276274
owner.name,
277275
EXISTS (SELECT FROM org_members WHERE org_members.organization_user_id = owner.id) AS is_org
278-
FROM projects p
276+
FROM projects_by_user_permission(#{callerUserId}, #{ProjectView}) AS p
279277
JOIN users owner ON owner.id = p.owner_user_id
280278
WHERE p.owner_user_id = #{projectOwnerUserId}
281-
AND user_has_project_permission(#{callerUserId}, p.id, #{ProjectView})
282-
ORDER BY p.created_at DESC
279+
ORDER BY p.created_at DESC, p.slug ASC
283280
|]
284281
where
285282
unpackRows :: [Project PG.:. FavData PG.:. ProjectOwner] -> [(Project, FavData, ProjectOwner)]
@@ -878,12 +875,11 @@ listContributorBranchesOfUserAccessibleToCaller contributorUserId mayCallerUserI
878875
project_owner.name,
879876
EXISTS (SELECT FROM org_members WHERE org_members.organization_user_id = project.owner_user_id)
880877
FROM project_branches b
881-
JOIN projects project ON project.id = b.project_id
878+
JOIN projects_by_user_permission(#{mayCallerUserId}, #{ProjectView}) AS project ON project.id = b.project_id
882879
JOIN users AS project_owner ON project_owner.id = project.owner_user_id
883880
WHERE
884881
b.deleted_at IS NULL
885882
AND b.contributor_id = #{contributorUserId}
886-
AND user_has_project_permission(#{mayCallerUserId}, b.project_id, #{ProjectView})
887883
|],
888884
branchNameFilter,
889885
cursorFilter,

share-api/src/Share/Postgres/Search/DefinitionSearch/Queries.hs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -401,13 +401,13 @@ globalDefNameCompletionSearch mayCaller mayUserFilter (Query query) limit = do
401401
[sql|
402402
WITH results(name, tag) AS (
403403
SELECT DISTINCT doc.name, doc.tag FROM global_definition_search_docs doc
404-
JOIN projects p ON p.id = doc.project_id
404+
JOIN projects_by_user_permission(#{mayCaller}, #{ProjectView}) p
405+
ON p.id = doc.project_id
405406
WHERE
406407
-- Find names which contain the query
407408
doc.name ILIKE ('%.' || like_escape(#{query}) || '%')
408409

409-
AND user_has_project_permission(#{mayCaller}, p.id, #{ProjectView})
410-
^{filters}
410+
^{filters}
411411
) SELECT r.name, r.tag FROM results r
412412
-- Docs and tests to the bottom, then
413413
-- prefer matches where the original query appears (case-matched),
@@ -501,11 +501,11 @@ globalDefinitionTokenSearch mayCaller mayUserFilter limit searchTokens preferred
501501
queryListRows @(ProjectId, ReleaseId, Name, Hasql.Jsonb)
502502
[sql|
503503
SELECT doc.project_id, doc.release_id, doc.name, doc.metadata FROM global_definition_search_docs doc
504-
JOIN projects p ON p.id = doc.project_id
504+
JOIN projects_by_user_permission(#{mayCaller}, #{ProjectView}) p
505+
ON p.id = doc.project_id
505506
WHERE
506507
-- match on search tokens using GIN index.
507508
tsquery(#{tsQueryText}) @@ doc.search_tokens
508-
AND user_has_project_permission(#{mayCaller}, p.id, #{ProjectView})
509509
AND (#{preferredArity} IS NULL OR doc.arity >= #{preferredArity})
510510
^{filters}
511511
^{namesFilter}
@@ -619,11 +619,10 @@ globalDefinitionNameSearch mayCaller mayUserFilter limit (Query query) = do
619619
queryListRows @(ProjectId, ReleaseId, Name, Hasql.Jsonb)
620620
[sql|
621621
SELECT doc.project_id, doc.release_id, doc.name, doc.metadata FROM global_definition_search_docs doc
622-
JOIN projects p ON p.id = doc.project_id
622+
JOIN projects_by_user_permission(#{mayCaller}, #{ProjectView}) p ON p.id = doc.project_id
623623
WHERE
624624
-- We may wish to adjust the similarity threshold before the query.
625625
#{query} <% doc.name
626-
AND user_has_project_permission(#{mayCaller}, p.id, #{ProjectView})
627626
^{filters}
628627
-- Score matches by:
629628
-- - projects in the catalog

share-api/src/Share/Postgres/Tickets/Queries.hs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -251,10 +251,9 @@ listTicketsByUserId callerUserId userId limit mayCursor mayStatusFilter = do
251251
ticket.author_id,
252252
(SELECT COUNT(*) FROM comments comment WHERE comment.ticket_id = ticket.id AND comment.deleted_at IS NULL) as num_comments
253253
FROM tickets AS ticket
254-
JOIN projects AS project ON project.id = ticket.project_id
254+
JOIN projects_by_user_permission(#{callerUserId}, #{ProjectView}) AS project ON project.id = ticket.project_id
255255
WHERE
256256
ticket.author_id = #{userId}
257-
AND user_has_project_permission(#{callerUserId}, project.id, #{ProjectView})
258257
AND (#{mayStatusFilter} IS NULL OR ticket.status = #{mayStatusFilter}::ticket_status)
259258
AND ^{cursorFilter}
260259
ORDER BY ticket.updated_at DESC, ticket.id DESC
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
-- The previous user_has_project_permission function is called on _every_ project when doing global omnisearch,
2+
-- which is too slow.
3+
4+
-- Create a view which serves as join table for finding projects for which the user has a given permission, it's much faster
5+
-- than running a permission check for every private project when we need to discover the list of all projects a user
6+
-- has access to.
7+
--
8+
-- This special-cases the 'project:view' permission to be even faster, it's the most common case.
9+
CREATE FUNCTION projects_by_user_permission(arg_user_id UUID, arg_permission permission)
10+
-- Returns a subset of the projects table
11+
RETURNS SETOF projects AS $$
12+
-- Get all public projects and projects owned by the user,
13+
-- as well as all public projects.
14+
SELECT p.*
15+
FROM projects p
16+
WHERE p.owner_user_id = arg_user_id
17+
OR (arg_permission = 'project:view' AND NOT p.private)
18+
UNION
19+
SELECT
20+
p.*
21+
FROM org_members om
22+
JOIN projects p
23+
ON om.organization_user_id = p.owner_user_id
24+
JOIN roles r ON om.role_id = r.id
25+
WHERE om.member_user_id = arg_user_id
26+
AND arg_permission = ANY(r.permissions)
27+
-- All public projects are already included above if the permission is 'project:view'
28+
AND (p.private OR arg_permission <> 'project:view')
29+
UNION
30+
-- Include projects the user is a direct maintainer of
31+
SELECT
32+
p.*
33+
FROM users u
34+
JOIN role_memberships rm ON u.subject_id = rm.subject_id
35+
JOIN roles r ON rm.role_id = r.id
36+
JOIN projects p ON rm.resource_id = p.resource_id
37+
WHERE u.id = arg_user_id
38+
AND arg_permission = ANY(r.permissions)
39+
-- All public projects are already included above if the permission is 'project:view'
40+
AND (p.private OR arg_permission <> 'project:view')
41+
$$ LANGUAGE sql STABLE PARALLEL SAFE;
42+
43+
-- A better index for this query.
44+
CREATE INDEX idx_projects_by_owner_and_privacy
45+
ON projects (private, owner_user_id);

transcripts/share-apis/project-maintainers/out/read-maintainer-project-list-after.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,32 @@
33
{
44
"createdAt": "<TIMESTAMP>",
55
"isFaved": false,
6-
"numFavs": 1,
6+
"numFavs": 0,
77
"owner": {
88
"handle": "@test",
99
"name": null,
1010
"type": "user"
1111
},
12-
"slug": "publictestproject",
13-
"summary": "test project summary",
12+
"slug": "privatetestproject",
13+
"summary": "private summary",
1414
"tags": [],
1515
"updatedAt": "<TIMESTAMP>",
16-
"visibility": "public"
16+
"visibility": "private"
1717
},
1818
{
1919
"createdAt": "<TIMESTAMP>",
2020
"isFaved": false,
21-
"numFavs": 0,
21+
"numFavs": 1,
2222
"owner": {
2323
"handle": "@test",
2424
"name": null,
2525
"type": "user"
2626
},
27-
"slug": "privatetestproject",
28-
"summary": "private summary",
27+
"slug": "publictestproject",
28+
"summary": "test project summary",
2929
"tags": [],
3030
"updatedAt": "<TIMESTAMP>",
31-
"visibility": "private"
31+
"visibility": "public"
3232
}
3333
],
3434
"status": [

0 commit comments

Comments
 (0)