I have a database design with multiple many-to-many relationships, and am trying to write a SELECT
statement that retrieves a result set with ONE ROW PER USER. To do this, I’d like to aggregate the permissions into a single column.
In the application’s database design, a user can have multiple permissions, a role can have multiple permissions, and a user can also have multiple roles, thereby compounding the user’s permissions.
Here are the relevant tables:
// Main tables
user:
- id
- username
- email
role:
- id
- name
- display_name
permission:
- id
- name
- display_name
// Many-to-many connector tables
user_role:
- user_id
- role_id
user_permission:
- user_id
- permission_id
(NOT GOOD) I’ve created a query that successfully aggregates the user permissions, as it’s just the single aggregate. However, I can’t seem to figure out how to get the 2-dimensional aggregate when adding on the roles’ permissions.
(BETTER) From the following query, I’d like to at least have a 6th column called "roles_permissions_array" that in pseudo-code is represented by: "For each role found in the ‘user_role_array’ column, select the array of all permissions."
(BEST) To take it further, it’d be nice to also have a version of the query that consolidates ALL permissions (both direct-user permissions and role-permissions) into a single column (de-duplicated), so we can see a single source of all permissions per user. In this scenario, the output columns would be: "user_id"
, "user_name"
, "user_email"
, "user_role_array"
, and "user_permissions_array"
. The last of these would be a consolidated array of all user permissions AND all role permissions
Here was the beginning of my attempt:
SELECT DISTINCT
-- "user" columns
u.id as "user_id", u.user_name as "user_user_name", u.email as "user_email",
p_agg.user_permissions_array,
r_agg.user_role_array
FROM users."user" u
-- REGULAR JOINS
LEFT JOIN users.user_role ur on ur.user_id = u.id
LEFT JOIN users.role_permission rp on ur.role_id = rp.role_id
-- START AGGREGATES
LEFT JOIN (
SELECT array_agg(r.name) as user_role_array, ur.user_id
FROM users.user_role ur
JOIN users.role r on r.id = ur.role_id
GROUP BY ur.user_id
) r_agg using(user_id)
LEFT JOIN (
SELECT array_agg(p.name) as user_permissions_array, up.user_id
FROM users.user_permission up
JOIN users.permission p on up.permission_id = p.id
GROUP BY up.user_id
) p_agg using(user_id)
-- rp_agg_parent
-- NOT WORKING FROM HERE DOWN
LEFT JOIN (
SELECT array_agg(role_permission_array) as two_dimensional_role_permissions_array
FROM (
SELECT p.role_id, r.name as "role_name", array_agg(p.permission_name) as "role_permission_array"
FROM (
SELECT r.id as "role_id", r.name as "role_name", p.name as "permission_name"
FROM users.role_permission rp
JOIN users.role r on r.id = rp.role_id
JOIN users.permission p on p.id = rp.permission_id
GROUP BY p.name, r.name, r.id
ORDER BY p.name
) as p
JOIN users.user_role ur on ur.role_id = p.role_id
JOIN users.role r on r.id = ur.role_id
GROUP BY r.name, p.role_id
order by role_name
) AS rp_agg_sub
JOIN users.role_permission rp on rp.role_id = rp_agg_sub.role_id
GROUP BY rp_agg_sub.role_name
) rp_agg_parent using(p_role_id)
order by u.user_name;
But I just get an error: "...column "p_role_id" specified in USING clause does not exist in left table"
. I suspect even if I got past this error, I might still not get back the desired result set. Any ideas?
2
Answers
The question is not 100% clear to me, but I interpret it as follows:
users
schema.Try this:
I suggest a slightly different order of joining and grouping tables.
First we collect all the permissions for user (user_id,permission_id) assigned directly to user or thru role.
Then join permission and role names and aggregate to arrays.
Finally, join user table.
See example
Demo