Delete rows depending on values from another table - sql

How to delete rows from my customer table depending on values from another table, let's say orders?
If the customer has no active orders they should be able to be deleted from the DB along with their rows (done using CASCADE). However, if they have any active orders at all, they can't be deleted.
I thought about a PLPGSQL function, then using it in a trigger to check, but I'm lost. I have a basic block of SQL shown below of my first idea of deleting the record accordingly. But it doesn't work properly as it deletes the customer regardless of status, it just needs one cancelled order in this function and not all cancelled.
CREATE OR REPLACE FUNCTION DelCust(int)
RETURNS void AS $body$
DELETE FROM Customer
WHERE CustID IN (
SELECT CustID
FROM Order
WHERE Status = 'C'
AND CustID = $1
);
$body$
LANGUAGE SQL;
SELECT * FROM Customer;
I have also tried to use a PLPGSQL function returning a trigger then using a trigger to help with the deletion and checks, but I'm lost on it. Any thoughts on how to fix this? Any good sources for further reading?

You were very close. I suggest you use NOT IN instead of IN:
DELETE FROM Customer
WHERE CustID = $1 AND
CustID NOT IN (SELECT DISTINCT CustID
FROM Order
WHERE Status = 'A');
I'm guessing here that Status = 'A' means "active order". If it's something else change the code appropriately.

Your basic function can work like this:
CREATE OR REPLACE FUNCTION del_cust(_custid int)
RETURNS int --③
LANGUAGE sql AS
$func$
DELETE FROM customer c
WHERE c.custid = $1
AND NOT EXISTS ( -- ①
SELECT FROM "order" o -- ②
WHERE o.status = 'C' -- meaning "active"?
AND o.custid = $1
);
RETURNING c.custid; -- ③
$func$;
① Avoid NOT IN if you can. It's inefficient and potentially treacherous. Related:
Select rows which are not present in other table
② "order" is a reserved word in SQL. Better chose a legal identifier, or you have to always double-quote.
③ I made it RETURNS int to signify whether the row was actually deleted. The function returns NULL if the DELETE does not go through. Optional addition.
For a trigger implementation, consider this closely related answer:
Foreign key contraints in many-to-many relationships

Related

Function is SQL

I want to create a function with a trigger on a relation. The trigger is supposed to not let more than 3 datasets inside of a relation. Somehow the function I created gives an exception regardless of how many datasets are inside of the Relation.
Here is my Code for the Function. so it gives back Null which apparently is enough to raise the exception. How do I make it so that when there are less than 3 it does not raise the exception?
create function myFunction() returns trigger as $$
Begin
if exists(
select case when count(*)>3 then count(*) end
from person
)
then raise exception 'Just 3 Datasets allowed';
End if;
return null;
End;
$$ language plpgsql
This construct:
if exists (select case when count(*)>3 then count(*) end
from person
)
always evaluates to true. Why? exists counts the number of rows returned by a subquery. A query that is an aggregation query with no group by always returns one row. If there are no rows in the underlying table, then the returned values would typically be NULL.
Note that exists doesn't care about the column values on the returned row. So a row with NULL values "exists" just as much as a row with other values. In fact, Postgres let's you return no columns at all! (Although I personally find that syntax a bit awkward.)
I suspect that you intend:
if (select count(*)
from person
) > 3

Oracle - Error at line 4: PL/SQL: SQL Statement

I am trying to build a trigger for my database and I am getting an error and I suspect it is because of my into clause but I am not getting the reason. I was reading the documentation of it (https://docs.oracle.com/cd/B19306_01/appdev.102/b14261/selectinto_statement.htm) and there is saying:
By default, a SELECT INTO statement must return only one row.
Otherwise, PL/SQL raises the predefined exception TOO_MANY_ROWS and
the values of the variables in the INTO clause are undefined. Make
sure your WHERE clause is specific enough to only match one row
Well, at least I am trying to be sure that my where clause is returning one and just one row. Can you give me some advice about it?
CREATE OR REPLACE TRIGGER t_ticket
INSTEAD OF INSERT ON V_buyTicket FOR EACH ROW
declare ticketID number; busy number; seatRoom number;
BEGIN
select count(a.id_ticket) into busy , s.freeSeats into seatRoom
from assigned a
inner join show e on (a.id_movie= e.id_movie)
inner join rooms s on (e.id_room = s.id_room)
where a.id_session = 1 AND a.id_movie = 1
group by s.freeSeats;
if(busy < seatRoom ) then
ticketID := seq_ticket.NEXTVAL;
insert into tickets(id_ticket, type, number, cc, store) values(ticketID, :new.type, :new.number, :new.cc, :new.store);
insert into assigned (id_ticket, id_movie, id_session) values(ticketID, :new.id_movie, :new.id_session);
else
DBMS_OUTPUT.PUT_LINE('No available seats');
end if;
END;
Don't use group by if you want exactly one row returned:
select count(a.id_ticket), sum(s.freeSeats)
into busy, seatRoom
from assigned a inner join
show e
on a.id_movie = e.id_movie inner join
rooms s
on e.id_room = s.id_room
where a.id_session = 1 and a.id_movie = 1;
An aggregation query without a group by always returns exactly one row. The alternative would be to include an explicit rownum = 1 in the WHERE clause.
My guess is that you conditions are not choosing exactly one room. You might want to check on the logic.

PostgreSQL - Check foreign key exists when doing a SELECT

Suppose I have the following data:
Table some_table:
some_table_id | value | other_table_id
--------------------------------------
1 | foo | 1
2 | bar | 2
Table other_table:
other_table_id | value
----------------------
1 | foo
2 | bar
Here, some_table has a foreign key to column other_table_id from other_table into the column of some name.
With the following query in PostgreSQL:
SELECT *
FROM some_table
WHERE other_table_id = 3;
As you see, 3 does not exists in other_table This query obviously will return 0 results.
Without doing a second query, is there a way to know if the foreign key that I am using as a filter effectively does not exist in the other_table?
Ideally as an error that later could be parsed (as it happends when doing an INSERT or an UPDATE with a wrong foreign key, for example).
You can exploit a feature of PL/pgSQL to implement this very cheaply:
CREATE OR REPLACE FUNCTION f_select_from_some_tbl(int)
RETURNS SETOF some_table AS
$func$
BEGIN
RETURN QUERY
SELECT *
FROM some_table
WHERE other_table_id = $1;
IF NOT FOUND THEN
RAISE WARNING 'Call with non-existing other_table_id >>%<<', $1;
END IF;
END
$func$ LANGUAGE plpgsql;
A final RETURN; is optional in this case.
The WARNINGis only raised if your query doesn't return any rows. I am not raising an ERROR in the example, since this would roll back the whole transaction (but you can do that if it fits your needs).
We've added a code example to the manual with Postgres 9.3 to demonstrate this.
If you perform an INSERT or UPDATE on some_table, specifying an other_table_id value that does not in fact exist in other_table, then you will get an error arising from violation of the foreign key constraint. SELECT queries are therefore your primary concern.
One way you could address the issue with SELECT queries would be to transform your queries to perform an outer join with other_table, like so:
SELECT st.*
FROM
other_table ot
LEFT JOIN some_table st ON st.other_table_id = ot.other_table_id
WHERE st.other_table_id = 3;
That query will always return at least one row if any other_table row has other_table_id = 3. In that case, if there is no matching some_table row, then it will return exactly one row, with that row having all columns NULL (given that it selects only columns from some_table), even columns that are declared not null.
If you want such queries to raise an error then you'll probably need to write a custom function to assist, but it can be done. I'd probably implement it in PL/pgSQL, using that language's RAISE statement.

Using stored procedure returning SETOF record in LEFT OUTER JOIN

I'm trying to call a stored procedure passing parameters in a left outer join like this:
select i.name,sp.*
from items i
left join compute_prices(i.id,current_date) as sp(price numeric(15,2),
discount numeric(5,2), taxes numeric(5,2)) on 1=1
where i.type = 404;
compute_prices() returns a setof record.
This is the message postgres shows:
ERROR: invalid reference to FROM-clause entry for table "i"
...left join compute_prices(i.id,current_date)...
HINT: There is an entry for table "i", but it cannot be referenced
from this part of the query.
This kind of query works in Firebird. Is there a way I could make it work by just using a query? I don't want to create another stored procedure that cycles through items and makes separate calls to compute_prices().
Generally, you can expand well known row types (a.k.a. record type, complex type, composite type) with the simple syntax #Daniel supplied:
SELECT i.name, (compute_prices(i.id, current_date)).*
FROM items i
WHERE i.type = 404;
However, if your description is accurate ...
The compute_prices sp returns a setof record.
... we are dealing with anonymous records. Postgres does not know how to expand anonymous records and throws an EXCEPTION in despair:
ERROR: a column definition list is required for functions returning "record"
PostgreSQL 9.3
There is a solution for that in Postgres 9.3. LATERAL, as mentioned by #a_horse in the comments:
SELECT i.name, sp.*
FROM items i
LEFT JOIN LATERAL compute_prices(i.id,current_date) AS sp (
price numeric(15,2)
,discount numeric(5,2)
,taxes numeric(5,2)
) ON TRUE
WHERE i.type = 404;
Details in the manual.
PostgreSQL 9.2 and earlier
Things get hairy. Here's a workaround: write a wrapper function that converts your anonymous records into a well known type:
CREATE OR REPLACE FUNCTION compute_prices_wrapper(int, date)
RETURNS TABLE (
price numeric(15,2)
,discount numeric(5,2)
,taxes numeric(5,2)
) AS
$func$
SELECT * FROM compute_prices($1, $2)
AS t(price numeric(15,2)
,discount numeric(5,2)
,taxes numeric(5,2));
$func$ LANGUAGE sql;
Then you can use the simple solution by #Daniel and just drop in the wrapper function:
SELECT i.name, (compute_prices_wrapper(i.id, current_date)).*
FROM items i
WHERE i.type = 404;
PostgreSQL 8.3 and earlier
PostgreSQL 8.3 has just reached EOL and is unsupported as of now (Feb. 2013).
So you'd better upgrade if at all possible. But if you can't:
CREATE OR REPLACE FUNCTION compute_prices_wrapper(int, date
,OUT price numeric(15,2)
,OUT discount numeric(5,2)
,OUT taxes numeric(5,2))
RETURNS SETOF record AS
$func$
SELECT * FROM compute_prices($1, $2)
AS t(price numeric(15,2)
,discount numeric(5,2)
,taxes numeric(5,2));
$func$ LANGUAGE sql;
Works in later versions, too.
The proper solution would be to fix your function compute_prices() to return a well know type to begin with. Functions returning SETOF record are generally a PITA. I only poke those with a five-meter-pole.
Assuming the compute_prices function always return a record with 3 prices, you could make its return type to TABLE (price numeric(15,2), discount numeric(5,2),taxes numeric(5,2)), and then I believe what you want could be expressed as:
SELECT i.name, (compute_prices(i.id,current_date)).*
FROM items i
WHERE i.type=404;
Note that its seems to me that LEFT JOIN ON 1=1 does not differ from an unconstrained normal JOIN (or CROSS JOIN), and I interpreted the question as actually unrelated to the left join.
I believe Daniel's answer will work also but haven't tried it yet. I do know that I have an SP called list_failed_jobs2 in a schema called logging, and a dummy table called Dual (like in Oracle) and the following statement works for me:
select * from Dual left join
(select * from logging.list_failed_jobs2()) q on 1=1;
Note, the SP call will not work without the parens, the correlation (q), or the ON clause. My SP returns a SETOF also.
Thus, I suspect something like this will work for you:
select i.name,sp.*
from items i
left join (select * from compute_prices(i.id,current_date)) as sp on 1=1
where i.type = 404;
Hope that helps.

PLpgSQL (or ANSI SQL?) Conditional calculation on a column

I want to write a stored procedure that performs a conditional calculation on a column. Ideally the implementation of the SP will be db agnostic - if possible. If not the underlying db is PostgreSQL (v8.4), so that takes precedence.
The underlying tables being queried looks like this:
CREATE TABLE treatment_def ( id PRIMARY SERIAL KEY,
name VARCHAR(16) NOT NULL
);
CREATE TABLE foo_group_def ( id PRIMARY SERIAL KEY,
name VARCHAR(16) NOT NULL
);
CREATE TABLE foo ( id PRIMARY SERIAL KEY,
name VARCHAR(16) NOT NULL,
trtmt_id INT REFERENCES treatment_def(id) ON DELETE RESTRICT,
foo_grp_id INT REFERENCES foo_group_def(id) ON DELETE RESTRICT,
is_male BOOLEAN NOT NULL,
cost REAL NOT NULL
);
I want to write a SP that returns the following 'table' result set:
treatment_name, foo_group_name, averaged_cost
where averaged cost is calcluated differently, depending on whether the row field *is_male* flag is set to true or false.
For the purpose of this question, lets assume that if the is_male flag is set to true, then the averaged cost is calculated as the SUM of the cost values for the grouping, and if the is_male flag is set to false, then the cost value is calculated as the AVERAGE of the cost values for the grouping.
(Obviously) the data is being grouped by trmt_id, foo_grp_id (and is_male?).
I have a rough idea about how to to write the SQL if there was no conditional test on the is_male flag. However, I could do with some help in writing the SP as defined above.
Here is my first attempt:
CREATE TYPE FOO_RESULT AS (treatment_name VARCHAR(16), foo_group_name VARCHAR(64), averaged_cost DOUBLE);
// Outline plpgsql (Pseudo code)
CREATE FUNCTION somefunc() RETURNS SETOF FOO_RESULT AS $$
BEGIN
RETURN QUERY SELECT t.name treatment_name, g.name group_name, averaged_cost FROM foo f
INNER JOIN treatment_def t ON t.id = f.trtmt_id
INNER JOIN foo_group_def g ON g.id = f.foo_grp_id
GROUP BY f.trtmt_id, f.foo_grp_id;
END;
$$ LANGUAGE plpgsql;
I would appreciate some help on how to write this SP correctly to implement the conditional calculation in the column results
Could look like this:
CREATE FUNCTION somefunc()
RETURNS TABLE (
treatment_name varchar(16)
, foo_group_name varchar(16)
, averaged_cost double precision)
AS
$BODY$
SELECT t.name -- AS treatment_name
, g.name -- AS group_name
, CASE WHEN f.is_male THEN sum(f.cost)
ELSE avg(f.cost) END -- AS averaged_cost
FROM foo f
JOIN treatment_def t ON t.id = f.trtmt_id
JOIN foo_group_def g ON g.id = f.foo_grp_id
GROUP BY 1, 2, f.is_male;
$BODY$ LANGUAGE sql;
Major points
I used an sql function, not plpgsql. You can use either, I just did it to shorten the code. plpgsql might be slightly faster, because the query plan is cached.
I skipped the custom composite type. You can do that simpler with RETURNS TABLE.
I would generally advise to use the data type text instead of varchar(n). Makes your life easier.
Be careful not to use names of the RETURN parameter without table-qualifying (tbl.col) in the function body, or you will create naming conflicts. That is why I commented the aliases.
I adjusted the GROUP BY clause. The original didn't work. (Neither does the one in #Ken's answer.)
You should be able to use a CASE statement:
SELECT t.name treatment_name, g.name group_name,
CASE is_male WHEN true then SUM(cost)
ELSE AVG(cost) END AS averaged_cost
FROM foo f
INNER JOIN treatment_def t ON t.id = f.trtmt_id
INNER JOIN foo_group_def g ON g.id = f.foo_grp_id
GROUP BY 1, 2, f.is_male;
I'm not familiar with PLpgSQL, so I'm not sure of the exact syntax for the BOOLEAN column, but the above should at least get you started in the right direction.