PostgreSQL: dynamically create result columns - postgresql

I want to "dynamically" create the result columns in a PostgreSQL query. I have these tables:
CREATE SEQUENCE users_id;
CREATE TABLE users (
id INT PRIMARY KEY NOT NULL DEFAULT NEXTVAL('users_id'),
name VARCHAR(128) NOT NULL
);
CREATE SEQUENCE quota_rules_id;
CREATE TABLE quota_rules (
id INT PRIMARY KEY NOT NULL DEFAULT NEXTVAL('quota_rules_id'),
user_id INT REFERENCES users(id),
rule VARCHAR(255) NOT NULL
);
CREATE INDEX user_id_index ON quota_rules(user_id);
INSERT INTO users (name) VALUES ('myname'); -- id=1
INSERT INTO quota_rules (user_id, rule) VALUES (1, 'a');
INSERT INTO quota_rules (user_id, rule) VALUES (1, 'b');
INSERT INTO quota_rules (user_id, rule) VALUES (1, 'c');
And want a query that returns this (1 row):
SELECT ............ user_id = 1;
name | quota_rule | quota_rule2 | quota_rule3
myname | a | b | c

Check out the crosstab function of the tablefunc module

Related

How to pull out records based on array of values

Suppose the following structure:
CREATE SCHEMA IF NOT EXISTS my_schema;
CREATE TABLE IF NOT EXISTS my_schema.user (
id SERIAL PRIMARY KEY,
tag_id BIGINT NOT NULL
);
CREATE TABLE IF NOT EXISTS my_schema.conversation (
id SERIAL PRIMARY KEY,
user_ids BIGINT[] NOT NULL
);
INSERT INTO my_schema.user VALUES
(1, 55555),
(2, 77777);
INSERT INTO my_schema.conversation VALUES
(1, '{1,2}');
I can pull out the my_schema.conversation records if I know the my_schema.user.id values:
SELECT *
FROM my_schema.conversation
WHERE user_ids #> '{1}'
The above works, but I need to use my_schema.user.tag_id instead of my_schema.user.id:
How can I do this?
Fiddle
You would have to join the two tables on the array values
SELECT *
FROM my_schema.user u
JOIN my_schema.conversation c
ON u.id = any(c.chat_ids)
WHERE u.tag_id=55555;

PostgreSQL does not drop partition cascade

I have a DB with 2 partitioned tables like
create table ppp (
id serial not null,
name varchar(255),
primary key (id)
);
insert into ppp (name) values ('ppp_first');
insert into ppp (name) values ('ppp_second');
create table rrr (
id serial not null,
ppp_id integer not null,
name varchar(255),
primary key (id, ppp_id),
foreign key (ppp_id) references ppp (id) on delete cascade
) partition by list(ppp_id);
create table rrr1 partition of rrr for values in (1);
create table rrr2 partition of rrr for values in (2);
create table sss (
id bigserial not null,
ppp_id integer not null,
rrr_id integer not null,
name varchar(255),
primary key (id, ppp_id),
foreign key (ppp_id, rrr_id) references rrr (ppp_id, id) on delete cascade
) partition by list(ppp_id);
create table sss1 partition of sss for values in (1);
create table sss2 partition of sss for values in (2);
insert into rrr (ppp_id, name) values (1, 'rrr_first');
insert into rrr (ppp_id, name) values (2, 'rrr_second');
insert into sss (ppp_id, rrr_id, name) values (1, 1, 'sss_first');
insert into sss (ppp_id, rrr_id, name) values (2, 2, 'sss_second');
I want to drop rrr1 with drop table rrr1 cascade;. It's dropped without errors but partition sss1 not dropped. So
=> select * from rrr;
id | ppp_id | name
----+--------+------------
2 | 2 | rrr_second
(1 row)
=> select * from sss;
id | ppp_id | rrr_id | name
----+--------+--------+------------
1 | 1 | 1 | sss_first
2 | 2 | 2 | sss_second
Why sss1 not dropped by cascade? Request to drop without cascade leads to error.

Is there a constraint between two tables to ensure a number is contained by a range?

I have two tables like these (ignoring the rest of fields and constraints):
CREATE TABLE table1 (
...
f1 UUID NOT NULL,
f2 UUID NOT NULL,
f3 UUID NOT NULL,
the_range INT8RANGE NOT NULL,
...
);
CREATE TABLE table2 (
...
g1 UUID NOT NULL,
g2 UUID NOT NULL,
g3 UUID NOT NULL,
the_number INT8 NOT NULL,
...
FOREIGN KEY (g1, g2, g3)
REFERENCES table1 (f1, f2, f3)
ON DELETE CASCADE
DEFERRABLE INITIALLY DEFERRED,
...
);
I need a constraint that ensures the_number <# the_range along with that FOREIGN KEY. How can I do it?
There is possibility of solution with a function used in a table constraint: the function takes the_number and the foreign key as parameters; the function returns true if it finds a matching row in table1 otherwise it return false.
Here is a simplified example:
create table table1(
key1 int primary key,
the_range int8range not null
);
create table table2(
the_number int8 not null,
fkey2 int references table1
);
--
create or replace function checkn(p_key int8, p_number int8) returns boolean
language plpgsql
as
$$
declare
v int;
begin
select key1 into v
from table1
where key1 = p_key and p_number <# the_range;
if v is not null
then return true;
else return false;
end if;
end;
$$;
--
alter table table2 add constraint fcheck check (checkn(fkey2, the_number));
Some testing:
create table table1(
key1 int primary key,
the_range int8range not null
);
CREATE TABLE
create table table2(
the_number int8 not null,
fkey2 int references table1
);
CREATE TABLE
create or replace function checkn(p_key int8, p_number int8) returns boolean
language plpgsql
as
$$
declare
v int;
begin
select key1 into v
from table1
where key1 = p_key and p_number <# the_range;
if v is not null
then return true;
else return false;
end if;
end;
$$;
CREATE FUNCTION
alter table table2 add constraint fcheck check (checkn(fkey2, the_number));
ALTER TABLE
insert into table1(key1, the_range) values (1, '[10,20]');
INSERT 0 1
insert into table1(key1, the_range) values (2, '[20,30]');
INSERT 0 1
select * from table1;
key1 | the_range
------+-----------
1 | [10,21)
2 | [20,31)
(2 rows)
insert into table2(the_number, fkey2) values(9, 1);
ERROR: new row for relation "table2" violates check constraint "fcheck"
DETAIL: Failing row contains (9, 1).
insert into table2(the_number, fkey2) values(21, 2);
INSERT 0 1
update table2 set the_number=33 where fkey2=2;
new row for relation "table2" violates check constraint "fcheck"
DETAIL: Failing row contains (33, 2).
select * from table2;
the_number | fkey2
------------+-------
21 | 2
(1 row)
My solution, up to now, is modifying table2 like this:
CREATE TABLE table2 (
...
g1 UUID NOT NULL,
g2 UUID NOT NULL,
g3 UUID NOT NULL,
the_range INT8RANGE NOT NULL,
the_number INT8 NOT NULL,
...
FOREIGN KEY (g1, g2, g3, the_range)
REFERENCES table1 (f1, f2, f3, the_range)
ON DELETE CASCADE
ON UPDATE CASCADE
DEFERRABLE INITIALLY DEFERRED,
CHECK (the_number <# the_range),
...
);
In this way, when you update the_range in table1, you'll change table2's the_range too. It's a redundancy mechanism that repeats the range in order to allow the check; it requires more space, but I prefer this over triggers.
I don't know if this is the best/safest way to do it, but it works.

Constraint, based on join with another table

I have table tariffs, with two columns: (tariff_id, reception)
I have table users, with two columns: (user_id, reception)
And I have table users_tariffs with two columns: (user_id, tariff_id).
I want to prevent situation when tariff from one reception is assigned to user from another reception. How can I do that?
E.G
Users:
user_id | reception
Putin | Russia
Trump | USA
Tariffs:
tariff_id | reception
cheap | USA
expensive | Russia
Wrong situation at users_tariffs, because Cheap tariff is for USA only:
user_id | tariff_id
Putin | Cheap
SOLUTION 1: FOREIGN KEY CONSTRAINTS
I am assuming the following table definitions.
In particular, the composite key in user_tariffs makes this a many-to-many relationship between users and tariffs.
CREATE TABLE tariffs (tariff_id int NOT NULL PRIMARY KEY,
reception text NOT NULL);
CREATE TABLE users (user_id int NOT NULL PRIMARY KEY,
reception text NOT NULL);
CREATE TABLE user_tariffs (tariff_id int NOT NULL REFERENCES tariffs (tariff_id),
user_id int NOT NULL REFERENCES users (user_id),
PRIMARY KEY (tariff_id, user_id));
You probably need a combination of all three columns somewhere, so let's create this:
ALTER TABLE user_tariffs ADD COLUMN reception text;
UPDATE user_tariffs a
SET reception = b.reception
FROM (SELECT * FROM tariffs) b
WHERE a.tariff_id = b.tariff_id;
ALTER TABLE user_tariffs ALTER COLUMN reception SET NOT NULL;
Now we can use FOREIGN KEY REFERENCES (user_id, reception) into users.
CREATE UNIQUE INDEX ON tariffs (tariff_id, reception);
ALTER TABLE user_tariffs ADD FOREIGN KEY (tariff_id, reception)
REFERENCES tariffs (tariff_id, reception);
In addition, we can use FK REFs (tariff_id, reception) into tariffs.
CREATE UNIQUE INDEX ON users (user_id, reception);
ALTER TABLE user_tariffs ADD FOREIGN KEY (user_id, reception)
REFERENCES users (user_id, reception);
Populate with data:
INSERT INTO users VALUES (1, 'cheap'), (2, 'expensive');
INSERT INTO tariffs VALUES (1, 'cheap'), (2, 'expensive');
Now assume we have the following data (user_id, tariff_id) to insert:
WITH data (user_id, tariff_id)
AS (VALUES (1, 2), (2, 1)), -- here is your application data
datas (user_id, tariff_id, reception)
AS (SELECT user_id,
tariff_id,
(SELECT u.reception -- reception calculated by user
FROM users u
WHERE u.user_id = d.user_id)
FROM data d)
INSERT INTO user_tariffs SELECT * FROM datas ;
Then you cannot insert the data, because you can only add (1, 1) or (2, 2) with the same reception, but not (1, 2) or (2, 1) with different reception's. The error message is:
ERROR: insert or update on table "user_tariffs" violates foreign key constraint "user_tariffs_user_id_fkey1"
DETAIL: Key (user_id, reception)=(2, cheap) is not present in table "users".
But you can insert with data AS VALUES (1, 1), (2, 2).
I think the FOREIGN KEY CONSTRAINT solution is to be preferred.
Please describe your functional dependencies, if you want better table designs.
SOLUTION 2: TRIGGER
-- DROP TABLE user_tariffs CASCADE;
-- DROP TABLE users CASCADE;
-- DROP TABLE tariffs CASCADE;
CREATE TABLE tariffs (tariff_id int NOT NULL PRIMARY KEY,
reception text NOT NULL);
CREATE TABLE users (user_id int NOT NULL PRIMARY KEY,
reception text NOT NULL);
CREATE TABLE user_tariffs (tariff_id int NOT NULL REFERENCES tariffs (tariff_id),
user_id int NOT NULL REFERENCES users (user_id),
PRIMARY KEY (tariff_id, user_id));
INSERT INTO users VALUES (1, 'cheap'), (2, 'expensive');
INSERT INTO tariffs VALUES (1, 'cheap'), (2, 'expensive');
-- table user_tariffs (user_id, tariff_id) only, without reception column.
Create a function with return type trigger:
CREATE OR REPLACE FUNCTION check_reception()
RETURNS trigger AS $$
DECLARE valid boolean := false;
BEGIN
SELECT (SELECT u.reception FROM users u WHERE u.user_id = NEW.user_id)
= (SELECT t.reception FROM tariffs t WHERE t.tariff_id = NEW.tariff_id)
INTO valid FROM user_tariffs ;
IF valid = false
THEN RAISE EXCEPTION '(user, tariff, reception) invalid.';
END IF;
RETURN NEW;
END; $$ LANGUAGE plpgsql ;
and register it:
CREATE TRIGGER reception_trigger
AFTER INSERT OR UPDATE ON user_tariffs
FOR EACH ROW EXECUTE PROCEDURE check_reception();
Now try to insert (1, 2), which would be (cheap, expensive) and is not allowed:
INSERT INTO user_tariffs VALUES (1, 2);
ERROR: (user, tariff, reception) invalid.
KONTEXT: PL/pgSQL function check_reception() line 7 at RAISE
But we can insert (1, 1), which is (cheap, cheap) without problem:
INSERT INTO user_tariffs VALUES (1, 1);
SELECT * FROM user_tariffs;
Remark
Triggers are not the best solution here, in my opinion. Try to avoid triggers, if possible. They can have side effects (transactions etc). Check StackOverflow for further details :)

Insert into table, return id and then insert into another table with stored id

I have the following three tables:
Please note that the below DDL came models generated by Django then grabbed out of Postgresql after they were created. So modifying the tables is not an option.
CREATE TABLE "parentTeacherCon_grade"
(
id INTEGER PRIMARY KEY NOT NULL,
"currentGrade" VARCHAR(2) NOT NULL
);
CREATE TABLE "parentTeacherCon_parent"
(
id INTEGER PRIMARY KEY NOT NULL,
name VARCHAR(50) NOT NULL,
grade_id INTEGER NOT NULL
);
CREATE TABLE "parentTeacherCon_teacher"
(
id INTEGER PRIMARY KEY NOT NULL,
name VARCHAR(50) NOT NULL
);
CREATE TABLE "parentTeacherCon_teacher_grade"
(
id INTEGER PRIMARY KEY NOT NULL,
teacher_id INTEGER NOT NULL,
grade_id INTEGER NOT NULL
);
ALTER TABLE "parentTeacherCon_parent" ADD FOREIGN KEY (grade_id) REFERENCES "parentTeacherCon_grade" (id);
CREATE INDEX "parentTeacherCon_parent_5c853be8" ON "parentTeacherCon_parent" (grade_id);
CREATE INDEX "parentTeacherCon_teacher_5c853be8" ON "parentTeacherCon_teacher" (grade_id);
ALTER TABLE "parentTeacherCon_teacher_grade" ADD FOREIGN KEY (teacher_id) REFERENCES "parentTeacherCon_teacher" (id);
ALTER TABLE "parentTeacherCon_teacher_grade" ADD FOREIGN KEY (grade_id) REFERENCES "parentTeacherCon_grade" (id);
CREATE UNIQUE INDEX "parentTeacherCon_teacher_grade_teacher_id_20e07c38_uniq" ON "parentTeacherCon_teacher_grade" (teacher_id, grade_id);
CREATE INDEX "parentTeacherCon_teacher_grade_d9614d40" ON "parentTeacherCon_teacher_grade" (teacher_id);
CREATE INDEX "parentTeacherCon_teacher_grade_5c853be8" ON "parentTeacherCon_teacher_grade" (grade_id);
My Question is: How do I write an insert statement (or statements) where I do not have keep track of the IDs? More specifically I have a teacher table, where teachers can teach relate to more than one grade and I am attempting to write my insert statements to start populating my DB. Such that I am only declaring a teacher's name, and grades they relate to.
For example, if I have a teacher that belong to only one grade then the insert statement looks like this.
INSERT INTO "parentTeacherCon_teacher" (name, grade_id) VALUES ('foo bar', 1 );
Where grades K-12 are enumerated 0,12
But Need to do something like (I realize this does not work)
INSERT INTO "parentTeacherCon_teacher" (name, grade_id) VALUES ('foo bar', (0,1,3) );
To indicate that this teacher relates to K, 1, and 3 grades
leaving me with this table for the parentTeacherCon_teacher_grade
+----+------------+----------+
| id | teacher_id | grade_id |
+----+------------+----------+
| 1 | 3 | 0 |
| 2 | 3 | 1 |
| 3 | 3 | 3 |
+----+------------+----------+
This is how I can currently (successfully) insert into the Teacher Table.
INSERT INTO public."parentTeacherCon_teacher" (id, name) VALUES (3, 'Foo Bar');
Then into the grade table
INSERT INTO public.parentTeacherCon_teacher_grade (id, teacher_id, grade_id) VALUES (1, 3, 0);
INSERT INTO public.parentTeacherCon_teacher_grade (id, teacher_id, grade_id) VALUES (2, 3, 1);
INSERT INTO public.parentTeacherCon_teacher_grade (id, teacher_id, grade_id) VALUES (3, 3, 3);
A bit more information.
Here is a diagram of the database
Other things I have tried.
WITH i1 AS (INSERT INTO "parentTeacherCon_teacher" (name) VALUES ('foo bar')
RETURNING id) INSERT INTO "parentTeacherCon_teacher_grade"
SELECT
i1.id
, v.val
FROM i1, (VALUES (1), (2), (3)) v(val);
Then I get this error.
[2016-08-10 16:07:46] [23502] ERROR: null value in column "grade_id" violates not-null constraint
Detail: Failing row contains (6, 1, null).
If you want to insert all three rows in one statement, you can use:
INSERT INTO "parentTeacherCon_teacher" (name, grade_id)
SELECT 'foo bar', g.grade_id
FROM (SELECT 0 as grade_id UNION ALL SELECT 1 UNION ALL SELECT 3) g;
Or, if you prefer:
INSERT INTO "parentTeacherCon_teacher" (name, grade_id)
SELECT 'foo bar', g.grade_id
FROM (VALUES (0), (2), (3)) g(grade_id);
EDIT:
In Postgres, you can have data modification statements as a CTE:
WITH i as (
INSERT INTO public."parentTeacherCon_teacher" (id, name)
VALUES (3, 'Foo Bar')
RETURNING *
)
INSERT INTO "parentTeacherCon_teacher" (name, teacher_id, grade_id)
SELECT 'foo bar', i.id, g.grade_id
FROM (VALUES (0), (2), (3)) g(grade_id) CROSS JOIN
i