Postgres table inheritance: move from parent to child and vice versa - postgresql

I am wondering how I can easily move data between a parent table and its child table in PostgreSQL (9.4) and vice versa.
Assume that I have the following database sample set up:
DROP TABLE IF EXISTS employee CASCADE;
DROP TABLE IF EXISTS director CASCADE;
CREATE TABLE employee(
id SERIAL PRIMARY KEY NOT NULL,
name VARCHAR(255) NOT NULL,
surname VARCHAR(255) NOT NULL,
employment_date DATE NOT NULL DEFAULT CURRENT_DATE
);
CREATE TABLE director(
director_id SERIAL PRIMARY KEY NOT NULL,
secretary_id INT4 REFERENCES employee(id),
extra_legal_benefits VARCHAR(255) ARRAY
) inherits (employee);
INSERT INTO employee(name, surname)
VALUES ('Alice', 'Alisson');
INSERT INTO employee(name, surname)
VALUES ('Bob', 'Bobson');
INSERT INTO employee(name, surname)
VALUES ('Carol', 'Clarckson');
INSERT INTO director(name, surname, secretary_id, extra_legal_benefits)
VALUES ('David', 'Davidson', 1, '{car, travel expenses}');
How can I promote (move) one of the employees to the director table (must no longer appear in the parent)?
How can I demote (move) one of the directors back to the employees table (must no longer apear in the child)?

Promote an employee:
with deleted as (
delete from only employee
where name = 'Carol'
returning *
)
insert into director (name, surname, secretary_id, extra_legal_benefits)
select name, surname, null, '{flight}'
from deleted;
However:
must no longer appear in the parent
Any row in the child table is by definition available in the parent table. You can only "hide" those rows if you use the predicate only when selecting from the employee table:
select *
from only employee;
The above will not show employees that are also director. A plain select * from employee however will show all names (but you can't distinguish them - that's the nature of inheritance).
Demote a director:
with deleted as (
delete from only director
where name = 'David'
returning *
)
insert into employee (name, surname)
select name, surname
from deleted;
But to be honest, I'd probably model this through an additional column (like position or role) on the employee entity instead of using inheritance. Or even a many-to-many relationship to a position (or role) entity as it is not uncommon that employees have multiple roles, e.g. in different departments, teams or other contexts.

Related

PostgreSQL: Is there a way to insert into multiple tables at once in order to satisfy RLS policies?

I have tables that represent relationships between different persons (legal and natural) by membership. The typical case being people being members of organizations. So, I did this:
CREATE TABLE
person (
id uuid PRIMARY KEY,
created_at timestamp with time zone DEFAULT now(),
modified_at timestamp with time zone,
card jsonb -- this contains the details in JSON inspired by vCard, e.g. '{ "fn": "Full Name", "url":"http://my.site.org/" }
);
CREATE TABLE
member (
organization uuid references person (id) on delete cascade not null,
member uuid references person (id) on delete cascade not null,
begin timestamp with time zone DEFAULT (now() AT TIME ZONE 'utc'),
"end" timestamp with time zone,
primary key (organization, member)
);
I am using RLS and I want to allow a user to insert a person into any organization they have write-permission to. For that, I have an SQL function can() that gives me all the organizations (person row IDs) they may write to. So, I wrote the following policy:
CREATE POLICY "Person can be created in organization with write-permission." ON person
FOR INSERT WITH CHECK (
EXISTS( SELECT 1 FROM member WHERE can('write') ? organization::text AND member = id )
);
My question: In order to fulfill this policy, I need to simultaneously insert the new person and the corresponding member link. Is this even possible?
I tried both
WITH new_member AS (
INSERT INTO member (organization, member)
VALUES ( 'cb00aaa9-4c95-4f1a-862e-21406d4d10e0', gen_random_uuid() )
RETURNING member )
INSERT INTO person ( id, card )
SELECT member, '{"fn":"Coherent Insert?"}'
FROM new_member;
and
WITH new_person as (
INSERT INTO person (id, card)
VALUES (gen_random_uuid(), '{"fn":"Coherent Insert?"}')
RETURNING id )
INSERT INTO member ( organization, member )
SELECT 'cb00aaa9-4c95-4f1a-862e-21406d4d10e0', id from new_person;
but both complain about:
ERROR: new row violates row-level security policy for table "person"
I could of course put both inserts into a SECURITY definer function without RLS and check privileges manually there but I am wondering if there is another way.

PostrgreSQL ForeignKeyViolation

I am attempting to insert some data into my database via a lambda function. I am getting the following error ForeignKeyViolation: insert or update on table "address" violates foreign key constraint "address_id_fkey"
I understand that this is because my address table has a foreign key linking it to the clients table, and the keys are not matching.
Is there a way to format my tables so that I can input my client data and address data together? Or will I need to input the client data first, then retrieve the id and use it to input the address data.
Currently I am running the following two functions.
postgres_insert_query = "INSERT INTO clients (name, phone, contact) VALUES ('{0}','{1}','{2}')".format(data['name'], data['phone'], data['contact'])
postgres_insert_query = "INSERT INTO address (line1, city, state, zip) VALUES ('{0}','{1}','{2}', {3})".format(address['line1'], address['city'], address['state'], address['zip'])
Even if no address data is present I would still like to create a row for it (with the correct foreign key).
use DEFERRABLE foreign key constraint. Then wrap you function into a transaction.
CREATE temp TABLE pktable (
id INT4 PRIMARY KEY,
other INT4
);
CREATE temp TABLE fktable (
id INT4 PRIMARY KEY,
fk INT4 REFERENCES pktable DEFERRABLE INITIALLY DEFERRED
);
BEGIN;
INSERT INTO fktable VALUES (100, 200);
INSERT INTO pktable VALUES (200, 500);
COMMIT;
Postgres allows DML operations within a CTE. Doing so will allow you to insert into both tables in a single statement while allowing auto-generation of both ids. The following is a Postgres implementation. See demo.
with thedata(name, phone, contact, line1, city, state, zip) as
( values ('client 1', 'ev4 4213', 'andy','614 a', 'some city;','that state','11111'))
, theinsert (cli_id) as
( insert into clients(name, phone, contact)
select name, phone, contact
from thedata
returning cli_id
)
insert into addresses(cli_id, line1, city, state, zip)
select cli_id, line1, city, state, zip
from theinsert
cross join thedata;
Unfortunately I do not know your obscurification (Orm) language but perhaps something like:
pg_query = "with thedata( {0} name, {1} phone, {2} contact, {3} line1, {4} city, {5} state, {6} zip) as
, theinsert (cli_id) as
( insert into clients(name, phone, contact)
select name, phone, contact
from thedata
returning cli_id
)
insert into addresses(cli_id, line1, city, state, zip)
select cli_id, line1, city, state, zip
from theinsert
cross join thedata".format(data['name'], data['phone'], data['contact']
, address['line1'], address['city'], address['state'], address['zip']);

How to update table on postgres with join statement

I have three tables on postgresql DB, and tried to update table but I failed to get result what I want. Please help me with getting valid result.
The first table is "employee".
On this table, the first three characters of "employee_id" mean employee type.
For example, employee_id="AA1-11111" is a member of employee_type="AA1".
employee_id
department
AA1-11111
A
AA1-22222
B
AB1-11111
A
The second table is "assessment".
On this table, assessment criteria is defined for (employee_type, department).
For example, an employee of employee_type="AA1" and department="A" will be evaluated by assessment_criteria="XX1X".
employee_type
department
assessment_criteria
AA1
A
XX1X
AA1
B
XX1Y
AA2
A
XX2X
The third table is "employee_assessment". On this table assessment_criteria for each employee is defined. (This table is calculated from "employee" and "assessment" by night batch processing.)
employee_id
department
assessment_criteria
AA1-11111
A
XX1X
AA1-22222
B
XX1Y
AB1-11111
A
Null
What I want to do is... to update "employee_assessment" table when "assessment" table is updated.
When "assessment" table is updated as like below...
employee_type
department
assessment_criteria
AA1
A
XX1X
AA1
B
NEW
AA2
A
Null
I want to update "employee_assessment" table like this.
employee_id
department
assessment_criteria
AA1-11111
A
XX1X
AA1-22222
B
NEW
AB1-11111
A
Null
I tried
UPDATE
employee_assessment
SET assessment_criteria=employee_assessment.assessment_criteria
FROM employee
LEFT JOIN (SELECT employee_id, LEFT(employee_id,3) as emp_type, department as emp_department from employee) as t1
ON
employee.employee_id=t1.employee_id
and
employee.department=t1.emp_department
left join assessment
on
t1.emp_type=assessment.employee_type
and
t1.emp_department=assessment.department;
But I got this result.
employee_id
department
assessment_criteria
AA1-11111
A
XX1X
AA1-22222
B
XX1X
AB1-11111
A
XX1X
My query seems to be wrong.
The actual cause of the problem is that you schema isn't properly normalized. Therefore you should solve this by fixing and normalizing your schema. Then you can simply use a view, that is "updated" automatically.
First have tables for the types and departments (unless you have that already (that's unclear)).
CREATE TABLE type
(id serial,
name varchar(64),
PRIMARY KEY (id));
CREATE TABLE department
(id serial,
name varchar(64),
PRIMARY KEY (id));
Then, in the table for the employees, just reference the types and departments. Don't have a column that actually are two columns, i.e. the type id has to have its own column and must not be concatenated to any other.
CREATE TABLE employee
(id serial,
type integer,
department integer,
given_name varchar(64),
surname varchar(64),
PRIMARY KEY (id),
FOREIGN KEY (type)
REFERENCES type
(id),
FOREIGN KEY (department)
REFERENCES department
(id));
In the table for the assessments reference the types and departments too.
CREATE TABLE assessment
(id serial,
type integer,
department integer,
name varchar(64),
criteria varchar(64),
PRIMARY KEY (id),
FOREIGN KEY (type)
REFERENCES type
(id),
FOREIGN KEY (department)
REFERENCES department
(id));
Now you can create view for the employee assessments that joins the data from the other tables and is always up to date. There's no need for any manual UPDATE.
CREATE VIEW employee_assessment
AS
SELECT e.id employee_id,
e.department employee_department,
a.criteria assessment_criteria
FROM employee e
LEFT JOIN assessment a
ON a.type = e.type
AND a.department = e.department;
A view also has the advantage that it cannot contain inconsistent data as the table you have now could.

Looking up values from many tables based on value in each column

I have several tables containing key value pairs for differint fields in my database. I also have a table that that contains the keys of these differint tables that should be selected as the value for that key. However, I can't figure out how to select these values from the multiple tables?
The tables
CREATE TABLE CHARACTERS(
ID INTEGER PRIMARY KEY,
NAME VARCHAR(64)
);
CREATE TABLE MEDIA(
ID INTEGER PRIMARY KEY,
NAME VARCHAR(64)
);
CREATE TABLE EPISODES(
ID INTEGER PRIMARY KEY,
MEDIAID INTEGER,
NAME VARCHAR(64)
);
-- Selecting from this table
CREATE TABLE APPS(
ID INTEGER PRIMARY KEY,
CHARID INTEGER,
EPISODEID INTEGER,
MEDIAID INTEGER
);
I am selecting from the APPS table, and I want to replace the value of the *ID columns with the value of the name in the accomping table's NAME column. I want this done for each row in the APPS table. Like so...
CHARID -> CHARACTERS.NAME
EPISODEID -> EPISODES.NAME
MEDIAID -> MEDIA.NAME
I have tried to use joins, but they don't do it for each row in the APPS table. I have 18 rows in the APPS table, but I only get back way less than I have in the table or way more than I have in the table. So how can I make it do it for each row in the APPS table?
You do by JOINing the tables together and selecting the desired columns from the individual tables:
SELECT c.name AS character_name, e.name AS episode, m.name AS media
FROM apps a
LEFT JOIN episodes e ON e.id = a.episodeid
LEFT JOIN media m ON m.id = a.mediaid
LEFT JOIN characters c ON c.id = a.charid;
If you want to present the rows in a specific order, you can specify that too as a final clause in the SELECT statement. You can use any field from the included tables; that field is not necessarily part of the columns selected:
ORDER BY a.id -- order by apps.id
or
ORDER BY e.id, c.name -- order first by episode id, then by character name
etc

How to implicitly insert SERIAL ID via view over more than one table

I have two tables, connected in E/R by a is-relation. One representing the "mother table"
CREATE TABLE PERSONS(
id SERIAL NOT NULL,
name character varying NOT NULL,
address character varying NOT NULL,
day_of_creation timestamp NOT NULL DEFAULT current_timestamp,
PRIMARY KEY (id)
)
the other representing the "child table"
CREATE TABLE EMPLOYEES (
id integer NOT NULL,
store character varying NOT NULL,
paychecksize integer NOT NULL,
FOREIGN KEY (id)
REFERENCES PERSONS(id),
PRIMARY KEY (id)
)
Now those two tables are joined in a view
CREATE VIEW EMPLOYEES_VIEW AS
SELECT
P.id,name,address,store,paychecksize,day_of_creation
FROM
PERSONS AS P
JOIN
EMPLOYEES AS E ON P.id = E.id
I want to write either a rule or a trigger to enable a db user to make an insert on that view, sparing him the nasty details of the splitted columns into different tables.
But I also want to make it convenient, as the id is a SERIAL and the day_of_creation has a default value there is no actual need that a user has to provide those, therefore a statement like
INSERT INTO EMPLOYEES_VIEW (name, address, store, paychecksize)
VALUES ("bob", "top secret", "drugstore", 42)
should be enough to result in
PERSONS
id|name|address |day_of_creation
-------------------------------
1 |bob |top secret| 2013-08-13 15:32:42
EMPLOYEES
id| store |paychecksize
---------------------
1 |drugstore|42
A basic rule would be easy as
CREATE RULE EMPLOYEE_VIEW_INSERT AS ON INSERT TO EMPLOYEE_VIEW
DO INSTED (
INSERT INTO PERSONS
VALUES (NEW.id,NEW.name,NEW.address,NEW.day_of_creation),
INSERT INTO EMPLOYEES
VALUES (NEW.id,NEW.store,NEW.paychecksize)
)
should be sufficient. But this will not be convenient as a user will have to provide the id and timestamp, even though it actually is not necessary.
How can I rewrite/extend that code base to match my criteria of convenience?
Something like:
CREATE RULE EMPLOYEE_VIEW_INSERT AS ON INSERT TO EMPLOYEES_VIEW
DO INSTEAD
(
INSERT INTO PERSONS (id, name, address, day_of_creation)
VALUES (default,NEW.name,NEW.address,default);
INSERT INTO EMPLOYEES (id, store, paychecksize)
VALUES (currval('persons_id_seq'),NEW.store,NEW.paychecksize)
);
That way the default values for persons.id and persons.day_of_creation will be the default values. Another option would have been to simply remove those columns from the insert:
INSERT INTO PERSONS (name, address)
VALUES (NEW.name,NEW.address);
Once the rule is defined, the following insert should work:
insert into employees_view (name, address, store, paychecksize)
values ('Arthur Dent', 'Some Street', 'Some Store', 42);
Btw: with a current Postgres version an instead of trigger is the preferred way to make a view updateable.