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

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$

Related

Run a stored procedure using select columns as input parameters?

I have a select query that returns a dataset with "n" records in one column. I would like to use this column as the parameter in a stored procedure. Below a reduced example of my case.
The query:
SELECT code FROM rawproducts
The dataset:
CODE
1
2
3
The stored procedure:
ALTER PROCEDURE [dbo].[MyInsertSP]
(#code INT)
AS
BEGIN
INSERT INTO PRODUCTS description, price, stock
SELECT description, price, stock
FROM INVENTORY I
WHERE I.icode = #code
END
I already have the actual query and stored procedure done; I just am not sure how to put them both together.
I would appreciate any assistance here! Thank you!
PS: of course the stored procedure is not as simple as above. I just choose to use a very silly example to keep things small here. :)
Here's two methods for you, one using a loop without a cursor:
DECLARE #code_list TABLE (code INT);
INSERT INTO #code_list SELECT code, ROW_NUMBER() OVER (ORDER BY code) AS row_id FROM rawproducts;
DECLARE #count INT;
SELECT #count = COUNT(*) FROM #code_list;
WHILE #count > 0
BEGIN
DECLARE #code INT;
SELECT #code = code FROM #code_list WHERE row_id = #count;
EXEC MyInsertSP #code;
DELETE FROM #code_list WHERE row_id = #count;
SELECT #count = COUNT(*) FROM #code_list;
END;
This works by putting the codes into a table variable, and assigning a number from 1..n to each row. Then we loop through them, one at a time, deleting them as they are processed, until there is nothing left in the table variable.
But here's what I would consider a better method:
CREATE TYPE dbo.code_list AS TABLE (code INT);
GO
CREATE PROCEDURE MyInsertSP (
#code_list dbo.code_list)
AS
BEGIN
INSERT INTO PRODUCTS (
[description],
price,
stock)
SELECT
i.[description],
i.price,
i.stock
FROM
INVENTORY i
INNER JOIN #code_list cl ON cl.code = i.code;
END;
GO
DECLARE #code_list dbo.code_list;
INSERT INTO #code_list SELECT code FROM rawproducts;
EXEC MyInsertSP #code_list = #code_list;
To get this to work I create a user-defined table type, then use this to pass a list of codes into the stored procedure. It means slightly rewriting your stored procedure, but the actual code to do the work is much smaller.
(how to) Run a stored procedure using select columns as input
parameters?
What you are looking for is APPLY; APPLY is how you use columns as input parameters. The only thing unclear is how/where the input column is populated. Let's start with sample data:
IF OBJECT_ID('dbo.Products', 'U') IS NOT NULL DROP TABLE dbo.Products;
IF OBJECT_ID('dbo.Inventory','U') IS NOT NULL DROP TABLE dbo.Inventory;
IF OBJECT_ID('dbo.Code','U') IS NOT NULL DROP TABLE dbo.Code;
CREATE TABLE dbo.Products
(
[description] VARCHAR(1000) NULL,
price DECIMAL(10,2) NOT NULL,
stock INT NOT NULL
);
CREATE TABLE dbo.Inventory
(
icode INT NOT NULL,
[description] VARCHAR(1000) NULL,
price DECIMAL(10,2) NOT NULL,
stock INT NOT NULL
);
CREATE TABLE dbo.Code(icode INT NOT NULL);
INSERT dbo.Inventory
VALUES (10,'',20.10,3),(11,'',40.10,3),(11,'',25.23,3),(11,'',55.23,3),(12,'',50.23,3),
(15,'',33.10,3),(15,'',19.16,5),(18,'',75.00,3),(21,'',88.00,3),(21,'',100.99,3);
CREATE CLUSTERED INDEX uq_inventory ON dbo.Inventory(icode);
The function:
CREATE FUNCTION dbo.fnInventory(#code INT)
RETURNS TABLE AS RETURN
SELECT i.[description], i.price, i.stock
FROM dbo.Inventory I
WHERE I.icode = #code;
USE:
DECLARE #code TABLE (icode INT);
INSERT #code VALUES (10),(11);
SELECT f.[description], f.price, f.stock
FROM #code AS c
CROSS APPLY dbo.fnInventory(c.icode) AS f;
Results:
description price stock
-------------- -------- -----------
20.10 3
40.10 3
Updated Proc (note my comments):
ALTER PROC dbo.MyInsertSP -- (1) Lose the input param
AS
-- (2) Code that populates the "code" table
INSERT dbo.Code VALUES (10),(11);
-- (3) Use CROSS APPLY to pass the values from dbo.code to your function
INSERT dbo.Products ([description], price, stock)
SELECT f.[description], f.price, f.stock
FROM dbo.code AS c
CROSS APPLY dbo.fnInventory(c.icode) AS f;
This ^^^ is how it's done.

Postgres - fill in missing data in new table

Given two tables, A and B:
A B
----- -----
id id
high high
low low
bId
I want to find rows in table A where bId is null, create an entry in B based off the data in A, and update the row in A to reference the newly created row. I can create the rows but I'm having trouble updating table A with the reference to the new row:
begin transaction;
with rows as (
insert into B (high, low)
select high, low
from A a
where a.bId is null
returning id as bId, a.id as aId
)
update A
set bId=(select bId from rows where id=rows.aId)
where id=rows.aId;
--commit;
rollback;
However, this fails with a cryptic error: ERROR: missing FROM-clause entry for table a.
Using a Postgres query, how can I achieve this?
either
update "A"
set "bId"=(select "bId" from rows where id=rows."aId")
without the where clause or
update "A"
set "bId"=(select "bId" from rows where id=rows."aId")
FROM rows
where "A".id=rows.aId;
I dont know if your tables realy have that names, as mentioned in the comments try to avoid uppercase tables and fieldnames and try to avoid reserved keynames.
I found a way to get it to work but I feel like it's not the most efficient.
begin transaction;
do $body$
declare
newId int4;
tempB record;
begin
create temp table TempAB (
High float8,
Low float8,
AID int4
);
insert into TempAB (High, Low, AId)
select high, low, id
from A
where bId is null;
for tempB in (select * from TempAB)
loop
insert into B (high, low)
values (tempB.high, tempB.low)
returning id into newId;
update A
set bId=newId
where id=tempB.AId;
end loop;
end $body$;
rollback;
--commit;

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');

Ensuring members of a link table share a common property

I'm using a link table to represent a many-to-many relationship as follows (slightly modified for my use case from this previous answer):
CREATE TABLE owner(
owner_id uuid DEFAULT gen_random_uuid(),
PRIMARY KEY(owner_id)
);
CREATE TABLE product(
product_id uuid DEFAULT gen_random_uuid(),
owner_id uuid NOT NULL,
PRIMARY KEY(product_id)
FOREIGN KEY(owner_id) REFERENCES owner(owner_id)
);
CREATE TABLE bill(
bill_id uuid DEFAULT gen_random_uuid(),
owner_id uuid NOT NULL,
PRIMARY KEY(bill_id),
FOREIGN KEY(owner_id) REFERENCES owner(owner_id)
);
CREATE TABLE bill_product(
bill_id uuid,
product_id uuid,
PRIMARY KEY(bill_id, product_id),
FOREIGN KEY(bill_id) REFERENCES bill(bill_id),
FOREIGN KEY(product_id) REFERENCES product(bill_id)
);
This will of course allow a given bill to belong to many products and vice versa. However, I am wondering what the best way is to ensure that the bill and product belong to the same owner.
I see two options:
Trigger - Have the owner of the bill and product checked BEFORE INSERT, e.g.
CREATE OR REPLACE FUNCTION verify_bill_product_owner() RETURNS trigger AS $trg$
BEGIN
IF (SELECT owner_id FROM product WHERE product_id = NEW.product_id)
<>
(SELECT owner_id FROM bill WHERE bill_id = NEW.bill_id)
THEN
RAISE EXCEPTION 'bill and product do not belong to different owners';
END IF;
RETURN NEW;
END
$trg$ LANGUAGE plpgsql;
CREATE TRIGGER tr_bill_product_biu
BEFORE INSERT OR UPDATE on bill_product
FOR EACH ROW
EXECUTE PROCECURE verify_bill_product_owner();
Compound foreign key - Add the owner_id to the bill_product table and have something like:
-- ..
owner_id uuid,
FOREIGN KEY(owner_id, bill_id) REFERENCES bill(owner_id, bill_id),
FOREIGN KEY(owner_id, product_id) REFERENCES product(product_id, product_id),
-- ..
I think both would work I'm just wondering which is most idiomatic and which would work best in a multi-client/session environment.
I'm using Postgres 9.4.2 :-)
The compound foreign key is cleaner, but requires more space and may have performance implications when the table gets large. The trigger results in the same effect, but I would rewrite the function as follows:
CREATE OR REPLACE FUNCTION verify_bill_product_owner() RETURNS trigger AS $trg$
BEGIN
PERFORM *
FROM product
JOIN bill USING (owner_id)
WHERE product_id = NEW.product_id AND bill_id = NEW.bill_id;
IF NOT FOUND THEN
RETURN NULL;
ELSE
RETURN NEW;
END IF;
END; $trg$ LANGUAGE plpgsql;

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;