PL/SQL insert based on conditions - plsql

I would appreciate all help I can get. I'm learning PL/SQL and have stumbled on a problem so please help me find an appropriate way of handling this situation :)
I'm running Oracle 11gR2
My schema:
CREATE TABLE "ENTRY"
(
"TYPE" VARCHAR2(5 CHAR) ,
"TRANSACTION" VARCHAR2(5 CHAR),
"OWNER" VARCHAR2(5 CHAR)
);
CREATE TABLE "VIEW"
(
"TYPE" VARCHAR2(5 CHAR) ,
"TRANSACTION" VARCHAR2(5 CHAR),
"OWNER" VARCHAR2(5 CHAR)
);
CREATE TABLE "REJECTED"
(
"TYPE" VARCHAR2(5 CHAR) ,
"TRANSACTION" VARCHAR2(5 CHAR),
"OWNER" VARCHAR2(5 CHAR)
);
My sample data:
insert into entry (type, transaction, owner) values (11111, 11111, 11111);
insert into entry (type, transaction, owner) values (22222, 22222, 22222);
Now for the puzzling part, I've wrote this procedure that should copy the values from the ENTRY table to VIEW table if a record does not exist for specific (transaction AND owner) combination. If such a combination exists in the VIEW table that record should then go to the REJECTED table. This procedure does that but on multiple runs of the procedure I get more and more entries in the REJECTED table so my question is how to limit inserts in the REJECTED table - if a record already exists in the REJECTED table then do nothing.
create or replace PROCEDURE COPY AS
v_owner_entry ENTRY.owner%TYPE;
v_transaction_entry ENTRY.transaction%TYPE;
v_owner VIEW.owner%TYPE;
v_transaction VIEW.transaction%TYPE;
begin
begin
select e.owner, e.transaction, v.owner, v.transaction
into v_owner_entry, v_transaction_entry, v_owner, v_transaction
from entry e, view v
where e.owner = v.owner
and e.transaction = v.transaction;
EXCEPTION
when too_many_rows
then
insert into REJECTED
(
TYPE,
TRANSACTION,
OWNER
)
SELECT
s1.TYPE,
s1.TRANSACTION,
s1.OWNER
FROM ENTRY s1;
when no_data_found
THEN
insert into VIEW
(
TYPE,
TRANSACTION,
OWNER
)
SELECT
s.TYPE,
s.TRANSACTION,
s.OWNER
FROM ENTRY s;
end;
end;
Any suggestions guys? :)
Cheers!
UPDATE
Sorry if the original post wasn't clear enough -
The procedure should replicate data (on a daily basis) from DB1 to DB2 and insert into VIEW or REJECTED depending on the conditions. Here is a photo, maybe it would be clearer:

I think Dmitry was trying to suggest using MERGE in the too_many_rows case of your exception handler. So you've already done the SELECT up front and determined that the Entry row appears in your View table and so it raises the exception too_many_rows.
The problem is that you don't know which records have thrown the exception (assuming your Entry table has more than one row easy time this procedure is called). So I think your idea of using the exception section to determine that you have too many rows was elegant, but insufficient for your needs.
As a journeyman programmer, instead of trying to come up with something terribly elegant, I'd use more brute force.
Something more like:
BEGIN
FOR entry_cur IN
(select e.owner, e.transaction, SUM(NVL2(v.owner, 1, 0)) rec_count
from entry e, view v
where e.owner = v.owner(+)
and e.transaction = v.transaction(+)
GROUP BY e.owner, e.transaction)
LOOP
CASE WHEN rec_count > 0
THEN INSERT INTO view
ELSE MERGE INTO rejected r
ON (r.transaction = entry_cur.transaction
AND r.owner = entry_cur.owner)
WHEN NOT MATCHED THEN INSERT blah blah blah
;
END LOOP;
END;
The HAVING COUNT(*) > 1 will
No exceptions thrown. The loop gives you the correct record you don't want to insert into View. BTW, I couldn't get over that you used keywords for object names - view, transaction, etc. You enquoted the table names on the CREATE TABLE "VIEW" statement, which gets around the fact that these are keywords, but you didn't when you referenced them later, so I'm surprised the compiler didn't reject the code. I think that's a recipe for disaster because it makes debugging so much harder. I"m just hoping that you did this for the example here, not in PL/SQL.
Personally, I've had trouble using the MERGE statement where it didn't seem to work consistently, but that was an Oracle version long ago and probably my own ignorance in how it should work.

Use MERGE statement:
merge into REJECTED r
using ENTRY e
on (r.type = e.type and
r.transaction = e.transaction and
r.owner = e.owner)
when not matched then insert (type, transaction, owner)
values (e.type, e.transaction, e.owner)
This query will insert into table REJECTED only combinations of (type, transaction, owner) from table ENTRY that are not present there yet.

You're trying to code yourself out of a quandary you've modeled yourself into.
A table should contain your entity. There should not be a table of entities in one state, another table for entities in another state, and yet another table for entities in a different state altogether. You're seeing the kind of problems this can lead to.
The state can be an attribute (field or column) of the one table. Or normalized to a state table but still only one entity table. When an entity changes states, this is accomplished by an update, not a move from one table to another.

Related

How To Create a PL/SQL Trigger That Detects an Inserted or Updated Row and updates a Record in a Different Table?

I am creating a book tracking database for myself that holds information about my books and allows me to keep track of who is borrowing them. I am trying to create a trigger on my Checkouts table that runs if a record is added or updated that will determine if a checkout data has been entered or if a checkin date has been entered and change the "available" field in my Books table to "Y" or "N".
I have created a trigger called "update_book_availablility" on my Checkouts table but I keep getting this error:
"PLS-00103: Encountered the symbol 'end-of-file' when expecting one of the following: ( begin case declare and exception exit for goto if loop mod null pragma raise return select update while with <<continue close current delete fetch lock insert open rollback savepoint set sql execute commit forall merge standard pipe purge json_object
Errors: check compiler log"
Here is my trigger code:
CREATE OR REPLACE NONEDITIONABLE TRIGGER "UPDATE_BOOK_AVAILABILITY"
AFTER INSERT OR UPDATE OF ISBN, PersonID, checkout_date, checkin_date
ON Checkouts
FOR EACH ROW
BEGIN
IF :NEW.checkout_date = NULL
THEN
UPDATE Book
SET available = 'N'
WHERE ISBN IN (SELECT :NEW.ISBN FROM Checkouts);
END IF;
END;
Here is an image of my ERD:
ERD
I have been looking into and double checking my trigger syntax, If condition syntax, subquery syntax, and googling this error but have found nothing that has helped. I am new to PL/SQL and would appreciate any help in understanding what I have done wrong or missed.
PLS-00103: Encountered the symbol end-of-file error is SYNTAX ERROR
Copied your trigger and adjusted it to one of my test tables - it works. I removed NONEDITIONABLE and changed trigger table name as well as column names and table/column beeing updated by trigger.
To Do:
Check your syntax again or write the trigger from scratch once more
"...WHERE ISBN IN (SELECT :NEW.ISBN FROM Checkouts)..." selects one fixed value (FOR EACH ROW) :NEW.ISBN of triggering table, better ->> "... WHERE ISBN = :NEW.ISBN ..."
Prety sure that you don't need NONEDITIONABLE trigger for your books tracking app...
Regards...

Efficient insertion of row and foreign table row if it does not exist

Similar to this question and this solution for PostgreSQL (in particular "INSERT missing FK rows at the same time"):
Suppose I am making an address book with a "Groups" table and a "Contact" table. When I create a new Contact, I may want to place them into a Group at the same time. So I could do:
INSERT INTO Contact VALUES (
"Bob",
(SELECT group_id FROM Groups WHERE name = "Friends")
)
But what if the "Friends" Group doesn't exist yet? Can we insert this new Group efficiently?
The obvious thing is to do a SELECT to test if the Group exists already; if not do an INSERT. Then do an INSERT into Contacts with the sub-SELECT above.
Or I can constrain Group.name to be UNIQUE, do an INSERT OR IGNORE, then INSERT into Contacts with the sub-SELECT.
I can also keep my own cache of which Groups exist, but that seems like I'm duplicating functionality of the database in the first place.
My guess is that there is no way to do this in one query, since INSERT does not return anything and cannot be used in a subquery. Is that intuition correct? What is the best practice here?
My guess is that there is no way to do this in one query, since INSERT
does not return anything and cannot be used in a subquery. Is that
intuition correct?
You could use a Trigger and a little modification of the tables and then you could do it with a single query.
For example consider the folowing
Purely for convenience of producing the demo:-
DROP TRIGGER IF EXISTS add_group_if_not_exists;
DROP TABLE IF EXISTS contact;
DROP TABLE IF EXISTS groups;
One-time setup SQL :-
CREATE TABLE IF NOT EXISTS groups (id INTEGER PRIMARY KEY, group_name TEXT UNIQUE);
INSERT INTO groups VALUES(-1,'NOTASSIGNED');
CREATE TABLE IF NOT EXISTS contact (id INTEGER PRIMARY KEY, contact TEXT, group_to_use TEXT, group_reference TEXT DEFAULT -1 REFERENCES groups(id));
CREATE TRIGGER IF NOT EXISTS add_group_if_not_exists
AFTER INSERT ON contact
BEGIN
INSERT OR IGNORE INTO groups (group_name) VALUES(new.group_to_use);
UPDATE contact SET group_reference = (SELECT id FROM groups WHERE group_name = new.group_to_use), group_to_use = NULL WHERE id = new.id;
END;
SQL that would be used on an ongoing basis :-
INSERT INTO contact (contact,group_to_use) VALUES
('Fred','Friends'),
('Mary','Family'),
('Ivan','Enemies'),
('Sue','Work colleagues'),
('Arthur','Fellow Rulers'),
('Amy','Work colleagues'),
('Henry','Fellow Rulers'),
('Canute','Fellow Ruler')
;
The number of values and the actual values would vary.
SQL Just for demonstration of the result
SELECT * FROM groups;
SELECT contact,group_name FROM contact JOIN groups ON group_reference = groups.id;
Results
This results in :-
1) The groups (noting that the group "NOTASSIGNED", is intrinsic to the working of the above and hence added initially) :-
have to be careful regard mistakes like (Fellow Ruler instead of Fellow Rulers)
-1 used because it would not be a normal value automatically generated.
2) The contacts with the respective group :-
Efficient insertion
That could likely be debated from here to eternity so I leave it for the fence sitters/destroyers to decide :). However, some considerations:-
It works and appears to do what is wanted.
It's a little wasteful due to the additional wasted column.
It tries to minimise the waste by changing the column to an empty string (NULL may be even more efficient, but for some can be confusing)
There will obviously be an overhead BUT in comparison to the alternatives probably negligible (perhaps important if you were extracting every Facebook user) but if it's user input driven likely irrelevant.
What is the best practice here?
Fences again. :)
Note Hopefully obvious, but the DROP statements are purely for convenience and that all other SQL up until the INSERT is run once
to setup the tables and triggers in preparation for the single INSERT
that adds a group if necessary.

Alternative to using subquery inside CHECK constraint?

I am trying to build a simple hotel room check-in database as a learning exercise.
CREATE TABLE HotelReservations
(
roomNum INTEGER NOT NULL,
arrival DATE NOT NULL,
departure DATE NOT NULL,
guestName CHAR(30) NOT NULL,
CONSTRAINT timeTraveler CHECK (arrival < departure) /* stops time travelers*/
/* CONSTRAINT multipleReservations CHECK (my question is about this) */
PRIMARY KEY (roomNum, arrival)
);
I am having trouble specifying a constraint that doesn't allow inserting a new reservation for a room that has not yet been vacated. For example (below), guest 'B' checks into room 123 before 'A' checks out.
INSERT INTO HotelStays(roomNum, arrival, departure, guestName)
VALUES
(123, date("2017-02-02"), date("2017-02-06"), 'A'),
(123, date("2017-02-04"), date("2017-02-08"), 'B');
This shouldn't be allowed but I am unsure how to write this constraint. My first attempt was to write a subquery in check, but I had trouble figuring out the proper subquery because I don't know how to access the 'roomNum' value of a new insert to perform the subquery with. I then also figured out that most SQL systems don't even allow subquerying inside of check.
So how am I supposed to write this constraint? I read some about triggers which seem like it might solve this problem, but is that really the only way to do it? Or am I just dense and missing an obvious way to write the constraint?
The documentation indeed says:
The expression of a CHECK constraint may not contain a subquery.
While it would be possible to create a user-defined function that goes back to the database and queries the table, the only reasonable way to implement this constraint is with a trigger.
There is a special mechanism to access the new row inside the trigger:
Both the WHEN clause and the trigger actions may access elements of the row being inserted, deleted or updated using references of the form "NEW.column-name" and "OLD.column-name", where column-name is the name of a column from the table that the trigger is associated with.
CREATE TRIGGER multiple_reservations_check
BEFORE INSERT ON HotelReservations
BEGIN
SELECT RAISE(FAIL, "reservations overlap")
FROM HotelReservations
WHERE roomNum = NEW.roomNum
AND departure > NEW.arrival
AND arrival < NEW.departure;
END;

How to describe user-input values in trigger - Oracle PL/SQL

I am learning PL/SQL and have attempted to create a trigger where once a particular condition has been met in the fact table, data will be routed to the appropriate dimension table.
There is a fact table
BHM_FACT_TABLE
Four dimension tables
BHM_EMPLOYEES
BHM_ACCOUNTS
BHM_SUBSCRIBERS
BHM_EXPENSES
In the fact table, there is an "S_Number" column that serves as the primary key for the fact table. There is also a "Code" column that lists either A, B, C, or D, depending on whether the record lists an employee, an account, a subscriber, or an expense (this is a very basic, rudimentary table, by the way).
The fact table only contains the following columns:
S_Number
Code
Employees
Accounts
Subscribers
Expenses.
The dimension tables contain additional columns to hold data about the employees, accounts, subscribers, and expenses (you can see the columns in details in the trigger below).
What I want the trigger to do is route the data that the user inputs to the appropriate dimension table, based on what is entered in the fact table - i.e., once a row is entered with a code of 'A', which means that this record is of an employee - then that will prompt the rest of the values input to go to the BHM_EMPLOYEES table. And so on and so forth. So, this is what I came up with:
CREATE OR replace TRIGGER bhm_test
AFTER INSERT ON bhm_fact_table
FOR EACH ROW
DECLARE
v_code VARCHAR2 (1);
BEGIN
SELECT code
INTO v_code
FROM bhm_fact_table;
IF v_code = 'A' THEN
INSERT INTO bhm_employees
(s_number,
code,
employeename,
employeeaddress,
employeessn,
employeephonenumber,
employeesalary)
VALUES ( :NEW.s_number,
:NEW.code,
employeename,
employeeaddress,
employeessn,
employeephonenumber,
employeesalary );
ELSIF v_code = 'B' THEN
INSERT INTO bhm_accounts
(s_number,
code,
accountname,
address,
taxid,
phonenumber,
invoicetotal)
VALUES ( :NEW.s_number,
:NEW.code,
accountname,
address,
taxid,
phonenumber,
invoicetotal );
ELSIF v_code = 'C' THEN
INSERT INTO bhm_subscribers
(s_number,
code,
subscribername,
subscriberaddress,
subscriberphone,
subscribertype,
subscriberpaid)
VALUES ( :NEW.s_number,
:NEW.code,
subscribername,
subscriberaddress,
subscriberphone,
subscribertype,
subscriberpaid );
ELSIF v_code = 'D' THEN
INSERT INTO bhm_expenses
(s_number,
code,
expensesname,
expensesamt)
VALUES ( :NEW.s_number,
:NEW.code,
expensesname,
expensesamt );
END IF;
END;
For this I am getting the error
ORA-00984: Column Not Allowed Here
for each last column listed for VALUES (employeesalary, invoicetotal, subscriberpaid, and expensesamt). I took away the bind variables for the columns that you see without them; I originally had them by all of the values, but got the "Bad Bind Variables" error.
So my first question is, what would I put in the VALUES clause, if I don't know that information yet and it won't be stored (and thus available to reference) from the fact table?
My second question is - am I even going remotely in the right direction with this trigger?
Well, you're trying to fetch from the same table you're inserting in, so at the very least you'll run into a Mutating table/trigger error.
You don't need to SELECT the code, it's accessible via :NEW.CODE in your trigger.
The other problem is that you're trying to insert into the various tables without referencing the source -
INSERT INTO bhm_accounts
(s_number,
code,
accountname,
address,
taxid,
phonenumber,
invoicetotal)
VALUES ( :NEW.s_number,
:NEW.code,
accountname,
address,
taxid,
phonenumber,
invoicetotal );
This, for instance makes no sense because you're trying to refer the same column as the source of data that you're trying to insert into.
If the columns are available in BHM_FACT_TABLE, then you can refer them using :NEW.COLUMN_NAME modifier and insert them into the subsequent tables.
But since you mention that these columns are not present in BHM_FACT_TABLE, and that they are user entered, then a trigger is a wrong approach to this. Triggers are useful when you have the data stored/available readily beforehand.
Instead of using triggers, a better way is to use a stored procedure which will insert the data into the various tables based on the columns.

Oracle 11g VARRAY of OBJECTS

I have the following statements in Oracle 11g:
CREATE TYPE person AS OBJECT (
name VARCHAR2(10),
age NUMBER
);
CREATE TYPE person_varray AS VARRAY(5) OF person;
CREATE TABLE people (
somePeople person_varray
)
How can i select the name value for a person i.e.
SELECT somePeople(person(name)) FROM people
Thanks
I'm pretty sure that:
What you're doing isn't what I'd be doing. It sort of completely violates relational principles, and you're going to end up with an object/type system in Oracle that you might not be able to change once it's been laid down. The best use I've seen for SQL TYPEs (not PL/SQL types) is basically being able to cast a ref cursor back for pipelined functions.
You have to unnest the collection before you can query it relationally, like so:
SELECT NAME FROM
(SELECT SP.* FROM PEOPLE P, TABLE(P.SOME_PEOPLE) SP)
That'll give you all rows, because there's nothing in your specifications (like a PERSON_ID attribute) to restrict the rows.
The Oracle Application Developer's Guide - Object Relational Features discusses all of this in much greater depth, with examples.
To insert query:-
insert into people values (
person_varray(person('Ram','24'))
);
To select :-
select * from people;
SELECT NAME FROM (SELECT SP.* FROM PEOPLE P, TABLE(P.somePeople) SP)
While inserting a row into people table use constructor of
person_varray and then the constructor
of person type for each project.
The above INSERT command
creates a single row in people table.
select somePeople from people ;
person(NAME, age)
---------------------------------------------------
person_varray(person('Ram', 1),
To update the query will be:-
update people
set somePeople =
person_varray
(
person('SaAM','23')
)

Resources