I have two tables, one with a list of movies, and one with a list of dates when each movie is played. The movies list has the columns name, id, start_date and end_date, while id is a unique identifier.
The shows list (the one with the dates) has id,movie_id,date.
Every time I INSERT a new show, I'd like the movies list to be updated: If the show.date is before the movie.start_date, I'd like the start_date to be updated to the value of the show.date. Same goes for the end date - obviously if the show.date is after the movie.end_date.
The following rule is what I am stuck with: (NOTE: It would only set the start date if it worked, getting the end date done should be easy once this works...)
CREATE RULE "movies_start_date_setter" AS ON INSERT TO "shows"
WHERE movies.id = NEW.movie_id AND movies.start_date < NEW.date
DO (UPDATE movies SET start_date = NEW.date);
It returns: ERROR: missing FROM-clause entry for table "movies"
This error doesn't give me (as a beginner) any information where I'm missing a FROM clause and why.
Now since everybody seems to think a rule is a bad idea (and I tend to bow the pressure) I tried a trigger, which is absolutely totally new to me.
Using a trigger (highly recommended) it would look something like this:
CREATE OR REPLACE FUNCTION adjust_start_date()
RETURNS trigger AS
$BODY$
BEGIN
UPDATE movies SET start_date = NEW.date
WHERE id = NEW.movie_id AND start_date > NEW.date;
RETURN NEW;
END;
$BODY$
LANGUAGE 'plpgsql';
Followed by:
CREATE TRIGGER adjust_start_date_trigger
AFTER INSERT
ON shows
FOR EACH ROW
EXECUTE PROCEDURE adjust_start_date();
This trigger may or may not work, it solves my issue but doesn't answer my original question...
Anybody else out there smarter than me (I sure hope so!)?
The table movies is not known in where clause. Use:
create rule movies_start_date_setter as
on insert to shows do (
update movies
set start_date = new.date
where id = new.movie_id and start_date > new.date
);
The reson for the error message is that you reference table movies in the WHERE clause.
You could try with a query rewrite rule like this:
CREATE RULE movies_start_date_setter AS
ON INSERT TO shows DO ALSO
UPDATE movies
SET start_date = LEAST(NEW.date, start_date),
end_date = GREATEST(NEW.date, end_date)
WHERE id = NEW.movie_id
AND NEW.date NOT BETWEEN start_date AND end_date;
but that won't work with multi-line INSERTs like this:
INSERT INTO shows (id, movie_id, date) VALUES
(1, 42, <very small date>),
(2, 42, <very large date>);
because only one of the dates in the movies row with id = 42 will get changed.
Rules are tricky and hard to understand.
You are better off with a trigger.
Related
I have a table called CUSTOMERS from which I want to delete all entries that are not present in VALUE_CUSTOMERS. VALUE_CUSTOMERS is a Pipelined Table Function which builds on customers.
DELETE
(
SELECT *
FROM CUSTOMERS
LEFT JOIN
(
SELECT
1 AS DELETABLE,
VC.*
FROM
(
CUSTOMER_PACKAGE.VALUE_CUSTOMERS(TRUNC(SYSDATE) - 30)) VC
)
USING
(
FIRST_NAME, LAST_NAME, DATE_OF_BIRTH
)
WHERE
DELETABLE IS NULL
)
;
When I try to execute the statement, I get the Error:
ORA-01752: cannot delete from view without exactly one key-preserved
table
It looks like you're example has wrong syntax (lack of table keyword) - I've tested it on Oracle 12c, so maybe it works on newer ones. Below some ideas - based on Oracle 12c.
You've got multiple options here:
Save result of CUSTOMER_PACKAGE.VALUE_CUSTOMERS to some temporary table and use it in your query
CREATE GLOBAL TEMPORARY TABLE TMP_CUSTOMERS (
FIRST_NAME <type based on CUSTOMERS.FIRST_NAME>
, LAST_NAME <type based on CUSTOMERS.LAST_NAME>
, DATE_OF_BIRTH <type based on CUSTOMERS.DATE_OF_BIRTH>
)
ON COMMIT DELETE ROWS;
Then in code:
INSERT INTO TMP_CUSTOMERS(FIRST_NAME, LAST_NAME, DATE_OF_BIRTH)
SELECT VC.FIRST_NAME, VC.LAST_NAME, VC.DATE_OF_BIRTH
FROM TABLE(CUSTOMER_PACKAGE.VALUE_CUSTOMERS(TRUNC(SYSDATE) - 30)) VC
;
-- and then:
DELETE FROM CUSTOMERS C
WHERE NOT EXISTS(
SELECT 1
FROM TMP_CUSTOMERS TMP_C
-- Be AWARE that NULLs are not handled here
-- so it's correct only if FIRST_NAME, LAST_NAME, DATE_OF_BIRTH are not nullable
WHERE C.FIRST_NAME = TMP_C.FIRST_NAME
AND C.LAST_NAME = TMP_C.LAST_NAME
AND C.DATE_OF_BIRTH = TMP_C.DATE_OF_BIRTH
)
;
-- If `CUSTOMER_PACKAGE.VALUE_CUSTOMERS` can return a lot of rows,
-- then you should create indexes on FIRST_NAME, LAST_NAME, DATE_OF_BIRTH
-- or maybe even 1 multi-column index on all of above columns.
Also, consider rewriting your query. You should insert into TMP_CUSTOMERS just a customer ID and then call a delete based on these ids.
The main risk here is that the data could be changed between these 2 operations and you should consider that issue.
You can save result in collection variable and then do a bulk delete with forall loop.
If number of rows to delete could be big, then you should extend this example using LIMIT clause. Even with limit, you still could encounter some problems - like not enough space in UNDO. So this solution is good only for small amount of data. The risk here is the same as in example above.
DECALRE
type t_tab is table of number index by pls_integer;
v_tab t_tab;
BEGIN
SELECT CUSTOMER.ID -- I hope you have some kind of Primary key there...
BULK COLLECT INTO v_tab
FROM CUSTOMERS
LEFT JOIN
(
SELECT
1 AS DELETABLE,
VC.*
FROM TABLE(CUSTOMER_PACKAGE.VALUE_CUSTOMERS(TRUNC(SYSDATE) - 30)) VC
)
USING
(
FIRST_NAME, LAST_NAME, DATE_OF_BIRTH
)
WHERE
DELETABLE IS NULL
;
FORALL idx in 1..V_TAB.COUNT()
DELETE FROM CUSTOMERS
WHERE CUSTOMERS.ID = V_TAB(idx)
;
END;
/
Do it completely different - that's preferable.
For example: logic from CUSTOMER_PACKAGE.VALUE_CUSTOMERS move to a view and based on that view create a delete statement. Remember to change CUSTOMER_PACKAGE.VALUE_CUSTOMERS to use that new view also (DRY principle).
Here's the issue that I'm working through today. We have an ERP system in place and I need to set up an event to email Purchase Order Authorizers (POA) whenever they have a Purchase Order ready for approval. This is pretty easy to set up for purchase orders where we have a single authorizer. However, some purchase orders need to be reviewed by a group of authorizers, specifically our QA group, before they can get approved.
I can get a list of authorizers for our QA group using the following:
select authorize_id from purch_authorize_group_line where authorize_group_id = '30-QA-NUC';
This query returns 50 rows of data.
I can also get the USERID associated with an Authorize ID using the following:
SELECT purchase_authorizer_api.get_userid('30','104351') FROM DUAL;
What I can't figure out is how to pass all values from the first query into the second one. This query:
SELECT purchase_authorizer_api.get_userid('30',(select authorize_id from purch_authorize_group_line where authorize_group_id = '30-QA-NUC')) FROM DUAL;
returns the error "ORA-01427: single-row subquery returns more than one row."
So, what I'm wondering, is whether there's a way for me to pass all 50 values from the first query into the second query and get USERIDs for all 50 users. If those USERIDs then I can get a notification email out to the QA group when they have a PO ready to approve.
Maybe you're looking for this.
SELECT purchase_authorizer_api.get_userid('30', authorize_id)
FROM purch_authorize_group_line
WHERE authorize_group_id = '30-QA-NUC';
This should help -
DECLARE
V_VARIABLE VARCHAR2(1024);
BEGIN
FOR REC IN (SELECT AUTHORIZE_ID AS VAL FROM PURCH_AUTHORIZE_GROUP_LINE WHERE AUTHORIZE_GROUP_ID = '30-QA-NUC')
LOOP
SELECT PURCHASE_AUTHORIZER_API.GET_USERID('30',R.VAL) INTO V_VARIABLE FROM DUAL;
..
..
../* You code processing logic */
END LOOP;
END;
/
I've got the following database schema:
Movie (MovieID, Title, Year, Score, Votes) Actor(ActorID, Name, RecentDate)
Casting (MovieID, ActorID, Ordinal)
I'm trying to write a trigger so whenever I insert a tuple or update a tuple in the Casting table, the recent date value using SYSDATE should be updated in the Actor table for the actorid that is being used to insert or update a tuple in the Casting table.
Here is my code:
CREATE OR REPLACE TRIGGER updateDate
BEFORE INSERT OR UPDATE ON CASTING
FOR EACH ROW
BEGIN
SELECT RECENTDATE
FROM ACTOR
UPDATE ACTOR
SET RECENTDATE = SYSDATE
WHERE ACTORID = :new.ACTORID;
END;
/
I'm new to SQL and I know I probably could write this in an easier way.
Any ideas how to get this working?
Thanks for all the help.
I am creating a database of films, where you can insert Films(id, director,the year of publishing-integer...), Directors, Actors(id, fate of birth, date of death-dates)... I have a problem with triggers in it, because in the table actors i want to ensure, that when you you want to add a new actor his year of birth cannot be bigger that the year of film publishing. But with the trigger I have written, i cannot insert any new actor, because the date of birth cannot be compared to the films publishing year, because the film does not exist yet. And i cannot add any new film too, because i have the same trigger on directors-which is not working too, and the film has a foreign mandatory key-id director. It is a bit complicated, so I hope you will get what I mean
create or replace
trigger "XVIKD00"."DATUM_NARODENIA_HEREC"
BEFORE INSERT OR UPDATE
ON HERCI
FOR EACH ROW
DECLARE
l_ROK_VYDANIA filmy.ROK_VYD%TYPE;
BEGIN
SELECT FILMY.ROK_VYD
INTO l_ROK_VYDANIA
FROM FILMY
WHERE FILMY.ID_FILM = :new.ID_HEREC;
IF( (:new.dat_umr_her is not null) and( FILMY.ROK_VYD is not null)
extract( year from :new.dat_nar_her ) > l_ROK_VYDANIA )
THEN
RAISE_APPLICATION_ERROR(-2009,'Dátumy nie su v správnom časovom
slede');
END IF;
END;
You have a circular dependence between your tables, and this way you can't insert any records, because the others don't exist yet.
To handle this in a reasonable fashion you should have:
Movies
id
name
Publishing_Year
Director
id
name
birthDate
Actor
id
name
birthDate
This way, you can always create each one of those, without having any dependencies from the others.
Then when you want to say that some Director directed a Movie you would insert into a table like this:
movie_director
id
movie_id
director_id
When you want to say that some actor stared in a Movie you insert a record in a table like this:
movie_actor
id
movie_id
actor_id
And if you wanted to validate if the director or the actor is old enough to be in said movie, you would put the triggers that validate that in these two tables.
I came across the following table structure and I need to perform a certain type of query upon it.
id
first_name
last_name
address
email
audit_parent_id
audit_entry_type
audit_change_date
The last three fields are for the audit trail. There is a convention that says: all original entries have the value "0" for "audit_parent_id" and the value "master" for "audit_entry_type". All the modified entries have the value of their parent id for audit_parent_id" and the value "modified" for the "audit_entry_type".
Now what I want to do is to be able to get the original value and the modified value for a field and I want to make this with less queries possible.
Any ideas? Thank you.
Assuming a simple case, when you want to get the latest adress value change for the record with id 50, this query fits your needs.
select
p.id,
p.adress as original_address,
(select p1.adress from persons p1 where p1.audit_parent_id = p.id order by audit_change_date desc limit 1) as latest_address
from
persons p -- Assuming it's the table name
where
p.id = 50
But this assumes that, even if the address value doesn't change between one audit to the other, it remains the same in the field.
Here's another example, showing all persons that had an address change:
select
p.id,
p.adress as original_address,
(select p1.adress from persons p1 where p1.audit_parent_id = p.id order by audit_change_date desc limit 1) as latest_address
from
persons p -- Assuming it's the table name
where
p.audit_parent_id = 0
and
p.adress not like (select p1.adress from persons p1 where p1.audit_parent_id = p.id order by audit_change_date desc limit 1)
This can be solved with pure SQL in modern Postgres using WITH RECURSIVE.
For PostgreSQL 8.3, this plpgsql function does the job while it is also a decent solution for modern PostgreSQL. You want to ..
get the original value and the modified value for a field
The demo picks first_name as filed:
CREATE OR REPLACE FUNCTION f_get_org_val(integer
, OUT first_name_curr text
, OUT first_name_org text) AS
$func$
DECLARE
_parent_id int;
BEGIN
SELECT INTO first_name_curr, first_name_org, _parent_id
first_name, first_name, audit_parent_id
FROM tbl
WHERE id = $1;
WHILE _parent_id <> 0
LOOP
SELECT INTO first_name_org, _parent_id
first_name, audit_parent_id
FROM tbl
WHERE id = _parent_id;
END LOOP;
END
$func$ LANGUAGE plpgsql;
COMMENT ON FUNCTION f_get_org_val(int) IS 'Get current and original values for id.
$1 .. id';
Call:
SELECT * FROM f_get_org_val(123);
This assumes that all trees have a root node with parent_id = 0. No circular references, or you will end up with an endless loop. You might want to add a counter and exit the loop after x iterations.