Role basesd access control recursive SQL query - sql

I made the following role based access database schema which is able to hold roles, operations and types. A role can perform a specific operation on a type. Connections to users or types is not important here because this will be application specific. Every of these three tables can have as many parents as they want.
At the moment I'm struggling with a query which outputs every possible combination from the role_operation_type table.
Every role should inherit every permission on a types from the ancestors which can be more than one. In my opinion I need three nested recursive with queries for that or is there any faster way to achieve that?
My intention is to put that query in a view and select the needed values when a user requests an operation on a type.
Here is the database schema:
CREATE TABLE IF NOT EXISTS `role` (
`id` INTEGER PRIMARY KEY,
`name` VARCHAR NOT NULL UNIQUE
);
CREATE TABLE IF NOT EXISTS `role_role` (
`role_id` INTEGER NOT NULL REFERENCES `role`(`id`) ON UPDATE CASCADE ON DELETE CASCADE,
`parent_role_id` INTEGER NOT NULL REFERENCES `role`(`id`) ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT role_uq UNIQUE (`role_id`, `parent_role_id`),
CONSTRAINT role_chk CHECK(`role_id` != `parent_role_id`)
);
CREATE TABLE IF NOT EXISTS `operation` (
`id` INTEGER PRIMARY KEY,
`name` VARCHAR NOT NULL UNIQUE
);
CREATE TABLE IF NOT EXISTS `operation_operation` (
`operation_id` INTEGER NOT NULL REFERENCES `operation`(`id`) ON UPDATE CASCADE ON DELETE CASCADE,
`parent_operation_id` INTEGER NOT NULL REFERENCES `operation`(`id`) ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT operation_uq UNIQUE (`operation_id`, `parent_operation_id`),
CONSTRAINT operation_chk CHECK(`operation_id` != `parent_operation_id`)
);
CREATE TABLE IF NOT EXISTS `type` (
`id` INTEGER PRIMARY KEY,
`name` VARCHAR NOT NULL UNIQUE
);
CREATE TABLE IF NOT EXISTS `type_type` (
`type_id` INTEGER NOT NULL REFERENCES `type`(`id`) ON UPDATE CASCADE ON DELETE CASCADE,
`parent_type_id` INTEGER NOT NULL REFERENCES `type`(`id`) ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT type_uq UNIQUE (`type_id`, `parent_type_id`),
CONSTRAINT type_chk CHECK(`type_id` != `parent_type_id`)
);
CREATE TABLE IF NOT EXISTS `role_operation_type` (
`role_id` INTEGER NOT NULL REFERENCES `role`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
`operation_id` INTEGER NOT NULL REFERENCES `operation`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
`type_id` INTEGER NOT NULL REFERENCES `type`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT role_id_operation_id_type_id_uq UNIQUE (`role_id`, `operation_id`, `type_id`)
);
CREATE VIEW IF NOT EXISTS role_role_recursive_view AS
WITH RECURSIVE p(role_id, r, parent_role_id) AS (
SELECT ROLE.id, ROLE.id, role_role.parent_role_id
FROM ROLE
INNER JOIN role_role ON ROLE.id = role_role.role_id
UNION
SELECT p.r, p.role_id, role_role.parent_role_id
FROM p
INNER JOIN role_role ON p.parent_role_id = role_role.role_id
WHERE p.r != role_role.parent_role_id
)
SELECT p.role_id, p.parent_role_id FROM p ORDER BY role_id;
CREATE VIEW IF NOT EXISTS operation_operation_recursive_view AS
WITH RECURSIVE o(operation_id, o, parent_operation_id) AS (
SELECT operation.id, operation.id, operation_operation.parent_operation_id
FROM operation
INNER JOIN operation_operation ON operation.id = operation_operation.operation_id
UNION
SELECT o.o, o.operation_id, operation_operation.parent_operation_id
FROM o
INNER JOIN operation_operation ON o.parent_operation_id = operation_operation.operation_id
WHERE o.o != operation_operation.parent_operation_id
)
SELECT o.operation_id, o.parent_operation_id FROM o ORDER BY operation_id;
CREATE VIEW IF NOT EXISTS type_type_recursive_view AS
WITH RECURSIVE t(type_id, t, parent_type_id) AS (
SELECT TYPE.id, TYPE.id, type_type.parent_type_id
FROM TYPE
INNER JOIN type_type ON TYPE.id = type_type.type_id
UNION
SELECT t.t, t.type_id, type_type.parent_type_id
FROM t
INNER JOIN type_type ON t.parent_type_id = type_type.type_id
WHERE t.t != type_type.parent_type_id
)
SELECT t.type_id, t.parent_type_id FROM t ORDER BY type_id;

Now I will answer to my own question with a solution which works quite well. This is a recursive query and generates every possible combination. This is a recursive query which might be slow when the inheritance level becomes deeper.
CREATE VIEW IF NOT EXISTS role_operation_type_recursive_view AS
WITH RECURSIVE T(role_id, operation_id, type_id) AS (
WITH RECURSIVE O(role_id, operation_id, type_id) AS (
WITH RECURSIVE R(role_id, operation_id, type_id) AS (
SELECT role_id, operation_id,type_id
FROM role_operation_type
UNION
SELECT role_role_recursive_view.role_id, R.operation_id, R.type_id
FROM R
INNER JOIN role_role_recursive_view ON R.role_id = role_role_recursive_view.parent_role_id
)
SELECT * FROM R
UNION
SELECT O.role_id,operation_operation_recursive_view.parent_operation_id ,O.type_id
FROM O
INNER JOIN operation_operation_recursive_view ON O.operation_id = operation_operation_recursive_view.operation_id
)
SELECT * FROM O
UNION
SELECT T.role_id, T.operation_id, type_type_recursive_view.type_id
FROM T
INNER JOIN type_type_recursive_view ON T.type_id = type_type_recursive_view.parent_type_id
)
SELECT * FROM T;

Related

Postgres many-to-many relation: finding rows all having relation to a set of rows from related table

Consider three tables, let's call them groups, subgroups, another_groups and table subgroups_another_groups that is specifying many-to-many relation between subgroups and another_groups. subgroups and groups are in one-to-many relation, so subgroups has foreign key group_id.
How is it possible to select another_groups that all of subgroups within a group have relation to?
I assume that you are speaking of such a setup:
CREATE TABLE groups (
id integer PRIMARY KEY
);
CREATE TABLE subgroups (
id integer PRIMARY KEY,
group_id integer REFERENCES groups NOT NULL
);
CREATE INDEX ON subgroups(group_id);
CREATE TABLE another_groups (
id integer PRIMARY KEY
);
CREATE TABLE subgroups_another_groups (
subgroup_id integer REFERENCES subgroups NOT NULL,
another_groups_id integer REFERENCES another_groups NOT NULL,
PRIMARY KEY(subgroup_id, another_groups_id)
);
CREATE INDEX ON subgroups_another_groups(another_groups_id);
Then you want to know all another_groups that are connected to a groups via the other two tables except the ones where there is a subgroup that does not have a connection to this another_groups, right?
In SQL, that would read:
SELECT DISTINCT g.id, a.id
FROM another_groups a
JOIN subgroups_another_groups sag ON a.id = sag.another_groups_id
JOIN subgroups s ON sag.subgroup_id = s.id
JOIN groups g ON s.group_id = g.id
WHERE NOT EXISTS
(SELECT 1 FROM subgroups s1
WHERE s1.group_id = g.id
AND NOT EXISTS
(SELECT 1 FROM subgroups_another_groups sag1
WHERE sag1.subgroup_id = s1.id
AND sag1.another_groups_id = a.id
)
);

Get rows that no foreign keys point to

I have two tables
CREATE TABLE public.city_url
(
id bigint NOT NULL DEFAULT nextval('city_url_id_seq'::regclass),
url text,
city text,
state text,
country text,
common_name text,
CONSTRAINT city_url_pkey PRIMARY KEY (id)
)
and
CREATE TABLE public.email_account
(
id bigint NOT NULL DEFAULT nextval('email_accounts_id_seq'::regclass),
email text,
password text,
total_replied integer DEFAULT 0,
last_accessed timestamp with time zone,
enabled boolean NOT NULL DEFAULT true,
deleted boolean NOT NULL DEFAULT false,
city_url_id bigint,
CONSTRAINT email_accounts_pkey PRIMARY KEY (id),
CONSTRAINT email_account_city_url_id_fkey FOREIGN KEY (city_url_id)
REFERENCES public.city_url (id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION
)
I want to come up with a query that fetches rows in the city_url only if there is no row in the email_account pointing to it with the city_url_id column.
NOT EXISTS comes to mind:
select c.*
from city_url c
where not exists (select 1
from email_account ea
where ea.city_url_id = c.id
);
There's also this option:
SELECT city_url.*
FROM city_url
LEFT JOIN email_account ON email_account.city_url_id = city_url.id
WHERE email_account.id IS NULL
A NOT EXISTS is absolutely the answer to the "... if there is no row ...".
Nonetheless it would be preferable to accomplish this by selecting then difference quantity.
Which is in principle:
SELECT a.*
FROM table1 a
LEFT JOIN table2 b
ON a.[columnX] = b.[columnY]
WHERE b.[columnY] IS NULL
Using the tablenames here, this would be:
SELECT c.*
FROM city_url c
LEFT JOIN email_account e
ON c.id = e.city_url
WHERE e.city_url IS NULL
I believe NOT IN could be used here as well, although this might be less performant on large datasets:
SELECT *
FROM city_url
WHERE id NOT IN (
SELECT city_url_id FROM email_account
)

Write a query using INNER JOIN AND OR

I have a form and an insert button and when i click on the button - the fields goes in to these tables (i didn't put here all the fields because they are not important for my question).
The Tables:
CREATE TABLE SafetyAct (
SafetyAct_id int identity(1,1),
Username varchar(50),
SafetyType_id int,
constraint pk_SafetyAct_id
primary key (SafetyAct_id),
constraint fk_Users_SafetyAct
foreign key(Username)
references Users(Username)
on delete cascade
)
CREATE TABLE Product (
Product_id int identity(1,1) primary key,
SafetyAct_id int,
Cause_id int,
constraint fk_SafetyAct_Product
foreign key(SafetyAct_id)
references SafetyAct(SafetyAct_id)
on delete cascade,
constraint fk_Cause_Product
foreign key(Cause_id)
references Cause(Cause_id)
on delete cascade
)
CREATE TABLE SafetyIntervention (
SafetyIntervention_id int identity(1,1) primary key,
SafetyAct_id int,
Cause_id int,
constraint fk_SafetyAct_SafetyIntervention
foreign key(SafetyAct_id)
references SafetyAct(SafetyAct_id)
on delete cascade,
constraint fk_Cause_SafetyIntervention
foreign key(Cause_id)
references Cause(Cause_id)
on delete cascade
)
CREATE TABLE Cause (
Cause_id int primary key,
Cause_name varchar(80)
)
I want to write a query that shows the fields - SafetyAct_id and Cause_name.
in the Cause_name field i have a problem because i want that the query will show me the cause name drom the Product table or from the SafetyIntervension table (of course to connect it to the Cause table because i have only the cause_id - foriegn key in these tables) and i don't know how to write INNER JOIN and OR at the same query.
I am new with this so plase be patient.
Thank you!
SELECT SA.SafetyAct_id,
C.Cause_name
FROM SafetyAct SA
LEFT JOIN Product P
ON SA.SafetyAct_id = P.SafetyAct_id
LEFT JOIN SafetyIntervention SI
ON SA.SafetyAct_id = SI.SafetyAct_id
LEFT JOIN Cause C
ON ISNULL(P.Cause_id,SI.Cause_id) = C.Cause_id
An or is easy. So, based on Lamak's code:
SELECT SA.SafetyAct_id,
C.Cause_name
FROM SafetyAct SA LEFT JOIN
Product P
ON SA.SafetyAct_id = P.SafetyAct_id LEFT JOIN
SafetyIntervention SI
ON SA.SafetyAct_id = SI.SafetyAct_id LEFT JOIN
Cause C
ON C.Cause_id = P.Cause_id OR C.Cause_Id = SI.Cause_id;
--------------------------------^
You can just use OR in the ON condition.
However, it is often challenging for a SQL engine (SQL Server included) to optimize a join when the on condition includes or or functions on the columns used for the join. Hence, the following is often more efficient:
SELECT SA.SafetyAct_id,
COALESCE(Cp.Cause_name, Csi.Cause_Name) as Cause_Name
FROM SafetyAct SA LEFT JOIN
Product P
ON SA.SafetyAct_id = P.SafetyAct_id LEFT JOIN
SafetyIntervention SI
ON SA.SafetyAct_id = SI.SafetyAct_id LEFT JOIN
Cause Cp
ON Cp.Cause_id = P.Cause_id LEFT JOIN
Cause Csi
ON Csi.Cause_Id = SI.Cause_id;
If only some of the records have a cause of either type, then add:
WHERE Cp.Cause_Id IS NOT NULL OR Csi.Cause_Id IS NOT NULL;

SQL constraints and ON DELETE

Given an simple example of foreign key constraints:
CREATE TABLE products (
product_no integer PRIMARY KEY,
name text,
price numeric
);
CREATE TABLE orders (
order_id integer PRIMARY KEY,
shipping_address text,
...
);
CREATE TABLE order_items (
product_no integer REFERENCES products ON DELETE CASCADE,
order_id integer REFERENCES orders ON DELETE CASCADE,
quantity integer,
PRIMARY KEY (product_no, order_id)
);
When a row is deleted from products or orders, referencing rows from order_items are also deleted.
But - it is possible to have the database detect orphaned rows from products or orders (rows for which there is no referencing order_item) and delete them? I know I could set up a query to do it easily enough, but in a larger more complex schema, that could be a lot of queries. I'm wondering if there's a similar mechanism to ON DELETE CASCADE?
Here is an example without reference counts:
DROP SCHEMA tmp CASCADE;
CREATE SCHEMA tmp ;
SET search_path=tmp;
CREATE TABLE one
( id SERIAL NOT NULL PRIMARY KEY
, name VARCHAR
);
CREATE TABLE two
( id SERIAL NOT NULL PRIMARY KEY
, name VARCHAR
);
CREATE TABLE onetwo
( one_id INTEGER REFERENCES one(id)
, two_id INTEGER REFERENCES two(id)
, PRIMARY KEY(one_id, two_id)
);
CREATE INDEX onetwo_rev ON onetwo (two_id,one_id);
-- Populate the tables.
INSERT INTO one(name) select 'One_name_' || gs::text FROM generate_series(1,5) gs ;
INSERT INTO two(name) select 'Two_name_' || gs::text FROM generate_series(1,5) gs ;
INSERT INTO onetwo (one_id, two_id)
SELECT o.id, t.id
FROM one o
JOIN two t ON 1=1
;
-- Remove some random associations between one&two,
-- and remove any unreferenced records from one and two
DELETE FROM onetwo WHERE random() < 0.7;
DELETE FROM one dd WHERE NOT EXISTS(SELECT * FROM onetwo ot WHERE ot.one_id=dd.id) ;
DELETE FROM two dd WHERE NOT EXISTS(SELECT * FROM onetwo ot WHERE ot.two_id=dd.id) ;
SELECT * FROM onetwo;
SELECT * FROM one;
SELECT * FROM two;
CREATE OR REPLACE FUNCTION check_the_deletes() RETURNS TRIGGER AS $meat$
BEGIN
DELETE FROM one dd
WHERE dd.id=OLD.one_id
AND NOT EXISTS(SELECT * FROM onetwo ot WHERE ot.one_id=dd.id)
;
DELETE FROM two dd
WHERE dd.id=OLD.two_id
AND NOT EXISTS(SELECT * FROM onetwo ot WHERE ot.two_id=dd.id)
;
RETURN NEW;
END; $meat$
LANGUAGE plpgsql;
CREATE TRIGGER update_last_sale
AFTER DELETE ON onetwo
FOR EACH ROW
EXECUTE PROCEDURE check_the_deletes()
;
SELECT o.name AS name1
, t.name AS name2
FROM onetwo ot
JOIN one o ON o.id = ot.one_id
JOIN two t ON t.id = ot.two_id
;
-- Delete some random associations
-- the trigger should also remove any unreferenced rows
-- from one and two tables.
DELETE FROM onetwo WHERE random() < 0.4;
SELECT o.name AS name1
, t.name AS name2
FROM onetwo ot
JOIN one o ON o.id = ot.one_id
JOIN two t ON t.id = ot.two_id
;
SELECT * FROM one;
SELECT * FROM two;

Including a set of rows in a view column

Design:
A main table where each entry in it can have zero of more of a set of options “checked”. It seems to me that it would be easier to maintain (adding/removing options) if the options were part of a separate table and a mapping was made between the main table and an options table.
Goal:
A view that contains the information from the main table, as well as all options to which that row has been mapped. However the latter information exists in the view, it should be possible to extract the option’s ID and its description easily.
The implementation below is specific to PostgreSQL, but any paradigm that works across databases is of interest.
The select statement that does what I want is:
WITH tmp AS (
SELECT
tmap.MainID AS MainID,
array_agg(temp_options) AS options
FROM tstng.tmap
INNER JOIN (SELECT id, description FROM tstng.toptions ORDER BY description ASC) AS temp_options
ON tmap.OptionID = temp_options.id
GROUP BY tmap.MainID
)
SELECT tmain.id, tmain.contentcolumns, tmp.options
FROM tstng.tmain
INNER JOIN tmp
ON tmain.id = tmp.MainID;
However, attempting to create a view from this select statement generates an error:
column "options" has pseudo-type record[]
The solution that I’ve found is to cast the array of options (record[]) to a text array (text[][]); however, I’m interested in knowing if there is a better solution out there.
For reference, the create instruction:
CREATE OR REPLACE VIEW tstng.vsolution AS
WITH tmp AS (
SELECT
tmap.MainID AS MainID,
array_agg(temp_options) AS options
FROM tstng.tmap
INNER JOIN (SELECT id, description FROM tstng.toptions ORDER BY description ASC) AS temp_options
ON tmap.OptionID = temp_options.id
GROUP BY tmap.MainID
)
SELECT tmain.id, tmain.contentcolumns, CAST(tmp.options AS text[][])
FROM tstng.tmain
INNER JOIN tmp
ON tmain.id = tmp.MainID;
Finally, the DDL in case my description has been unclear:
CREATE TABLE tstng.tmap (
mainid INTEGER NOT NULL,
optionid INTEGER NOT NULL
);
CREATE TABLE tstng.toptions (
id INTEGER NOT NULL,
description text NOT NULL,
unwanted_column text
);
CREATE TABLE tstng.tmain (
id INTEGER NOT NULL,
contentcolumns text
);
ALTER TABLE tstng.tmain ADD CONSTRAINT main_pkey PRIMARY KEY (id);
ALTER TABLE tstng.toptions ADD CONSTRAINT toptions_pkey PRIMARY KEY (id);
ALTER TABLE tstng.tmap ADD CONSTRAINT tmap_pkey PRIMARY KEY (mainid, optionid);
ALTER TABLE tstng.tmap ADD CONSTRAINT tmap_optionid_fkey FOREIGN KEY (optionid)
REFERENCES tstng.toptions (id);
ALTER TABLE tstng.tmap ADD CONSTRAINT tmap_mainid_fkey FOREIGN KEY (mainid)
REFERENCES tstng.tmain (id);
You could create composite type e.g. temp_options_type with:
DROP TYPE IF EXISTS temp_options_type;
CREATE TYPE temp_options_type AS (id integer, description text);
After that just cast temp_options to that type within array_agg, so it returns temp_options_type[] instead of record[]:
DROP VIEW IF EXISTS tstng.vsolution;
CREATE OR REPLACE VIEW tstng.vsolution AS
WITH tmp AS
(
SELECT
tmap.MainID AS MainID,
array_agg(CAST(temp_options AS temp_options_type)) AS options
FROM
tstng.tmap INNER JOIN
(
SELECT id, description
FROM tstng.toptions
ORDER BY description
) temp_options
ON tmap.OptionID = temp_options.id
GROUP BY tmap.MainID
)
SELECT tmain.id, tmain.contentcolumns, tmp.options
FROM tstng.tmain
INNER JOIN tmp ON tmain.id = tmp.MainID;
Example result:
TABLE tstng.vsolution;
id | contentcolumns | options
----+----------------+-----------------------
1 | aaa | {"(1,xxx)","(2,yyy)"}
2 | bbb | {"(3,zzz)"}
3 | ccc | {"(1,xxx)"}
(3 rows)