Get the table name of each row from inherited tables [duplicate] - postgresql

This question already has answers here:
Get the name of a row's source table when querying the parent it inherits from
(2 answers)
Closed 2 years ago.
Take the following query:
CREATE TEMP TABLE users
(
user_id SERIAL,
name varchar(50)
);
CREATE TEMP TABLE admins
(
section integer
) INHERITS(users);
INSERT INTO users (name) VALUES ('Kevin');
INSERT INTO admins (name, section) VALUES ('John', 1);
CREATE FUNCTION pg_temp.is_admin(INTEGER) RETURNS BOOLEAN AS
$$
DECLARE
result boolean;
BEGIN
SELECT INTO result COUNT(*) > 0
FROM admins
WHERE user_id = $1;
RETURN result;
END;
$$
LANGUAGE PLPGSQL;
SELECT name, pg_temp.is_admin(user_id) FROM users;
Is there any postgres feature that would allow me to get rid of the is_admin function? Basically to check the row class type (in terms of inheritance)?
I understand the table design isn't ideal, this is just to provide a simple example so I can find out if what I am after is possible.

You can use the tableoid hidden column for this.
SELECT tableoid, * FROM users;
or in this case:
SELECT tableoid = 'admins'::regclass AS is_admin, * FROM users;
Note, however, that this will fall apart horribly if you want to find a non-leaf membership, i.e. if there was superusers that inherited from admins, a superuser would be reported here with is_admin false.
AFAIK there's no test for "is member of a relation or any child relation(s)", though if you really had t you could get the oids of all the child relations with a subquery, doing a tableoid IN (SELECT ...).

Related

table-valued function in PostreSQL only returning one column [duplicate]

This question already has answers here:
Postgres function returning table not returning data in columns
(2 answers)
Return multiple columns and rows from a function PostgreSQL instead of record
(3 answers)
PL/pgSQL functions: How to return a normal table with multiple columns using an execute statement
(3 answers)
Closed 4 months ago.
I have the following schema in PostreSQL:
patients (patient_id, name, gender, dob)
clinics (clinic_id, name)
doctors (doctor_id, clinic_id, name, specialty)
examinations (patient_id, doctor_id, exam_date, exam_cost)
I want to create a table function that takes in a patient's ID as input and returns their examination history including the patient's name, the doctor's name, the exam date, the exam cost, and the name of the clinic. I created the following table function:
CREATE OR REPLACE FUNCTION exam_history (patient_id char(8))
RETURNS TABLE (
patient_name varchar(250),
doctor_name varchar(250),
exam_date timestamp,
exam_cost numeric,
clinic_name varchar(250)) AS
$$
BEGIN
ALTER TABLE patients
RENAME COLUMN name TO patient_name;
ALTER TABLE doctors
RENAME COLUMN name TO doctor_name;
ALTER TABLE clinics
RENAME COLUMN name TO clinic_name;
RETURN QUERY
SELECT p.patient_name, d.doctor_name, e.exam_date, e.exam_cost, c.clinic_name
FROM examinations e
JOIN patients p on p.patient_id = e.patient_id
JOIN doctors d on d.doctor_id = e.doctor_id
JOIN clinics c on c.clinic_id = d.clinic_id
WHERE p.patient_id = exam_history.patient_id;
END
$$
LANGUAGE plpgsql
This is my first time creating a table function so I'm not sure if it is entirely correct. When I use the function on a random patient's ID, I only get a one column outlook shown below:
How do I get the correct table output with 5 columns?
To decompose the returned row type, call the function with
SELECT * FROM exam_history ('12345678');
Related:
Simple PostgreSQL function to return rows

SQL if else in proc isn't working properly - beginner friendly

i'm pretty new to SQL and struggling with the last piece on my final project. Logically, I know how to do it (i think) I just can't get the syntax correct.
I have a teaches table and an instructor table. The teaches tables tells you what instructor has taught what class, when and for how many credits. The instructor table is obviously a table with IDs and instructor names. Finally, I have a table instructor_taught which really serves no purpose other than for us to learn. this table holds instructor's IDs and how many total credits they have taught.
My challenge is to create a proc that takes an instructor's ID and adds or updates their total credits taught in the instructor_taught table.
First: check if the ID already exists in the instructor_taught table...if it does not exist then I need to add it to the table. If it does exist then I need to update the entry and add the additional 3 credits. so if instructor 101 is already in the table with 3 total credits then I need to update the 3 to 6 instead of creating a new row.
I was able to create a proc that adds the instructor to the instructor_taught table but the update part is failing...so if i enter id:101 10x it will add that instructor 10x instead of updating.
Here is my code:
CREATE OR REPLACE PROCEDURE Day_21_monthlyPayment (IN id VARCHAR(30), INOUT d_count INTEGER DEFAULT 0)
LANGUAGE plpgsql
AS
$$
BEGIN
BEGIN
SELECT teaches.id, instructor.name, COUNT(*) AS d_count
FROM teaches NATURAL JOIN instructor
WHERE teaches.id = Day_21_monthlyPayment.id
GROUP BY teaches.id, instructor.name;
END
IF EXISTS(SELECT teaches.id FROM teaches)
THEN UPDATE instructor_course_nums SET tot_courses = d_count
WHERE teaches.id = Day_21_monthlyPayment.id
ELSE INSERT INTO instructor_course_nums (id, name, tot_courses)
END IF;
END;
$$
I"m pretty sure this is a simple if else statement but i can't get the syntax right. Thanks in advance!
DDL:
CREATE TABLE instructor(
ID VARCHAR(12),
name VARCHAR(30),
section VARCHAR(30),
salary NUMERIC(20)
)
CREATE TABLE teaches(
ID VARCHAR(12),
course VARCHAR(30),
count VARCHAR(30),
term VARCHAR(20),
year NUMERIC(20)
)
CREATE TABLE instructor_course_nums(
ID VARCHAR(12),
name VARCHAR(30),
tot_courses NUMERIC(2)
)
Even if you are forced to do a procedure, I think you can still do this in (nearly) one fell swoop with a single DML statement. Assuming you have the proper constraints (primary key), I would think something like this would work:
CREATE OR REPLACE PROCEDURE Day_21_monthlyPayment (idx VARCHAR(30), INOUT d_count INTEGER DEFAULT 0)
LANGUAGE plpgsql
AS
$BODY$
BEGIN
SELECT COUNT(*)
into d_count
FROM teaches t JOIN instructor i on t.id = i.id
WHERE t.id = idx;
insert into instructor_course_nums
select t.id, t.name, d_count
from instructor t
where t.id = idx
on conflict (id) do
update
set tot_courses = d_count;
END;
$BODY$

Saving a query in a table

I would like to store a table variable as to be accessed by another query within a function. Here is what I have so far.
CREATE OR REPLACE FUNCTION suggest(p_id INTEGER)
RETURNS TABLE (
r_id INT,
i_id INT
) AS $$
DECLARE
p_record INT[];
BEGIN
SELECT ingredient_id INTO p_record FROM shop_ingredients
WHERE item_id IN
(SELECT item_id FROM basket
WHERE user_id = p_id);
...
I am not sure whether the type of p-record should be an array of INTEGER or a RECORD. Within the function I would like to access such list of values, for example:
HAVING SUM(ingredient_id = ANY(p_record)) >= (COUNT(*)*0.6)
How can I achieve this? I have searched endlessly to understand how to manage this but to no avail.
The problem with creating a table, like so:
CREATE TEMP TABLE IF NOT EXISTS p_record
AS SELECT ingredient_id
FROM shop_ingredients
WHERE item_id IN
(SELECT item_id FROM basket
WHERE user_id = p_id);
is that if p_id parameter changes, the p_record variables does not.
Maybe the following SQL function can help:
create or replace function suggest(p_id int)
returns table (i_id int)
language sql
as
$$
select ingredient_id
from shop_ingredient
where item_id in
(select item_id from basket
where user_id = p_id);
$$;
Note that the SELECT statement column list must exactly match the TABLE clause after RETURNS keyword and this function is only SQL so no need of BEGIN/END block or intermediate record variable.

If existing record, row is returned, but if new record inserted, row is not returned

Two tables. author, and book
I am adding a Book into the book table.
If the Author is listed is already in the author table, then get the author's id and insert it into the Book row.
If the Author is not in the author table, then insert a new author and use the id to insert into the Book row.
This functionality works fine.
The database responds appropriately and with the code below (not the actual code, but a more refined version) and rows are appropriately referenced or created.
I also want the query to return the Book row and this is fine.
The Book row is always returned in all tested conditions, be it a Book with an existing author or a Book with a known author.
The issue comes when I now want to join it with the author table to get the author details back as well.
NOW ->
If I insert a Book with a known Author, the functionality is perfect and the row is returned perfectly as expected.
If I insert a Book with a NEW Author, the new author is still created, the new book is still inserted BUT ZERO rows are returned.
I am not sure why this is happening or how I would go about getting the row.
CREATE TABLE author (id PRIMARY KEY, name VARCHAR (255));
CREATE TABLE book (id PRIMARY KEY, title VARCAR (255), author REFERENCES author (id));
WITH
s AS (
SELECT id FROM author
WHERE name = 'British Col'
),
i AS (
INSERT INTO author(name)
SELECT ('Eoin Colfer')
WHERE NOT EXISTS (select 1 from s)
RETURNING id
),
j AS (
SELECT id FROM s
UNION ALL
SELECT id FROM i
),
ins AS (
INSERT INTO book
(title, author)
SELECT 'Artemis Fowl', j.id
FROM j
RETURNING *
)
SELECT ins.*, author.*
FROM ins
JOIN author
ON ins.author = author.id
;
Explanation
This has to do with the behavior of common table expressions in PostgreSQL.
Per the docs (https://www.postgresql.org/docs/current/queries-with.html):
The sub-statements in WITH are executed concurrently with each other
and with the main query. Therefore, when using data-modifying
statements in WITH, the order in which the specified updates actually
happen is unpredictable. All the statements are executed with the same
snapshot (see Chapter 13), so they cannot “see” one another's effects
on the target tables. This alleviates the effects of the
unpredictability of the actual order of row updates, and means that
RETURNING data is the only way to communicate changes between
different WITH sub-statements and the main query. An example of this
is that in
WITH t AS (
UPDATE products SET price = price * 1.05
RETURNING *
)
SELECT * FROM products;
the outer SELECT would return the original prices before the action of
the UPDATE...
The final sentence (below the code snippet) is critical.
Your query against the author table at the end returns data as it was before the insert statements within the CTEs.
Alternative Approach
An alternative approach would be to do this work in a function where you can use variables.
First, some suggested changes to your tables:
CREATE TABLE author
(
id SERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE -- Unique for ON CONFLICT later
);
CREATE TABLE book
(
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
author_id INT NOT NULL REFERENCES author (id),
UNIQUE (title, author_id) -- Prevent duplicates
);
Example function:
CREATE OR REPLACE FUNCTION add_book (in_book_title TEXT, in_author_name TEXT)
RETURNS TABLE
(
author_id INT,
book_id INT,
author_name TEXT,
book_title TEXT
)
AS $$
#variable_conflict use_column
DECLARE
var_author_id INT;
var_book_id INT;
BEGIN
-- Upsert author, return id
INSERT INTO author (name)
VALUES (in_author_name)
ON CONFLICT (name) DO
UPDATE SET name = EXCLUDED.name -- Do update to allow use of returning
RETURNING id INTO var_author_id;
-- Upsert book, return id
INSERT INTO book (title, author_id)
VALUES (in_book_title, var_author_id)
ON CONFLICT (title, author_id) DO
UPDATE SET title = EXCLUDED.title -- Do update to allow use of returning
RETURNING id INTO var_book_id;
-- Return the record using your join (similar)
RETURN QUERY
SELECT a.id, b.id, a.name, b.title
FROM author a
INNER JOIN book b
ON a.id = b.author_id
WHERE b.id = var_book_id;
END;
$$ LANGUAGE PLPGSQL VOLATILE;
Usage:
SELECT * FROM add_book('Artemis Fowl', 'Eoin Colfer');

Get row to swap tables on a certain condition

I currently have a parent table:
CREATE TABLE members (
member_id SERIAL NOT NULL, UNIQUE, PRIMARY KEY
first_name varchar(20)
last_name varchar(20)
address address (composite type)
contact_numbers varchar(11)[3]
date_joined date
type varchar(5)
);
and two related tables:
CREATE TABLE basic_member (
activities varchar[3])
INHERITS (members)
);
CREATE TABLE full_member (
activities varchar[])
INHERITS (members)
);
If the type is full the details are entered to the full_member table or if type is basic into the basic_member table. What I want is that if I run an update and change the type to basic or full the tuple goes into the corresponding table.
I was wondering if I could do this with a rule like:
CREATE RULE tuple_swap_full
AS ON UPDATE TO full_member
WHERE new.type = 'basic'
INSERT INTO basic_member VALUES (old.member_id, old.first_name, old.last_name,
old.address, old.contact_numbers, old.date_joined, new.type, old.activities);
... then delete the record from the full_member
Just wondering if my rule is anywhere near or if there is a better way.
You don't need
member_id SERIAL NOT NULL, UNIQUE, PRIMARY KEY
A PRIMARY KEY implies UNIQUE NOT NULL automatically:
member_id SERIAL PRIMARY KEY
I wouldn't use hard coded max length of varchar(20). Just use text and add a check constraint if you really must enforce a maximum length. Easier to change around.
Syntax for INHERITS is mangled. The key word goes outside the parens around columns.
CREATE TABLE full_member (
activities text[]
) INHERITS (members);
Table names are inconsistent (members <-> member). I use the singular form everywhere in my test case.
Finally, I would not use a RULE for the task. A trigger AFTER UPDATE seems preferable.
Consider the following
Test case:
Tables:
CREATE SCHEMA x; -- I put everything in a test schema named "x".
-- DROP TABLE x.members CASCADE;
CREATE TABLE x.member (
member_id SERIAL PRIMARY KEY
,first_name text
-- more columns ...
,type text);
CREATE TABLE x.basic_member (
activities text[3]
) INHERITS (x.member);
CREATE TABLE x.full_member (
activities text[]
) INHERITS (x.member);
Trigger function:
Data-modifying CTEs (WITH x AS ( DELETE ..) are the best tool for the purpose. Requires PostgreSQL 9.1 or later.
For older versions, first INSERT then DELETE.
CREATE OR REPLACE FUNCTION x.trg_move_member()
RETURNS trigger AS
$BODY$
BEGIN
CASE NEW.type
WHEN 'basic' THEN
WITH x AS (
DELETE FROM x.member
WHERE member_id = NEW.member_id
RETURNING *
)
INSERT INTO x.basic_member (member_id, first_name, type) -- more columns
SELECT member_id, first_name, type -- more columns
FROM x;
WHEN 'full' THEN
WITH x AS (
DELETE FROM x.member
WHERE member_id = NEW.member_id
RETURNING *
)
INSERT INTO x.full_member (member_id, first_name, type) -- more columns
SELECT member_id, first_name, type -- more columns
FROM x;
END CASE;
RETURN NULL;
END;
$BODY$
LANGUAGE plpgsql VOLATILE;
Trigger:
Note that it is an AFTER trigger and has a WHEN condition.
WHEN condition requires PostgreSQL 9.0 or later. For earlier versions, you can just leave it away, the CASE statement in the trigger itself takes care of it.
CREATE TRIGGER up_aft
AFTER UPDATE
ON x.member
FOR EACH ROW
WHEN (NEW.type IN ('basic ','full')) -- OLD.type cannot be IN ('basic ','full')
EXECUTE PROCEDURE x.trg_move_member();
Test:
INSERT INTO x.member (first_name, type) VALUES ('peter', NULL);
UPDATE x.member SET type = 'full' WHERE first_name = 'peter';
SELECT * FROM ONLY x.member;
SELECT * FROM x.basic_member;
SELECT * FROM x.full_member;