SQL Trigger to allow only customers who are old enough to buy a book? - sql

I have 3 tables, person, audiobook and audiobook_purchases. My database is running MariaDB.
person has fields: id, date_of_birth;
audiobook has fields: ISBN, age_rating, title;
audiobook_purchases has fields: ISBN, customer_id, date_of_purchase;
I'm trying to write a trigger to make sure that when a customer tried to purchases an audiobook, they are old enough to do so according to the age_rating in audiobook.
For example, If Harry Potter and the Philosipher's Stone had age rating 16 and customer Jennifer (ID 1) with date of birth 2010-01-01 tried to purchase this book, this would not be allowed, but Dominick(ID 2) with date_of_birth 1978-01-01 would be allowed.
Please could someone show me a way to run this trigger?

I don't know MariaDB in particular, so my answer may need some adjustments.
You want to create an insert trigger on audiobook_purchase so that a new order will be inserted only if the person who wants to place the order is old enough according to audiobook.age_rating.
First you need to figure out a way of extracting the year from person.date_of_birth. Something like the YEAR() scalar function will probably be available. MariaDB may also provide a NOW() function, which gives the current date. So the person age right now will be: YEAR(NOW()) - YEAR(person.date_of_birth).
Then you have to write the insert trigger. The tricky part is to query the person table to get the person date_of_birth from his id, then to compare it to audiobook.age_rating.
Let's set out an example. First we declare the tables schemas:
CREATE TABLE person(id, name, date_of_birth);
CREATE TABLE audiobook(isbn, age_rating, title);
CREATE TABLE audiobook_purchases(isbn, customer_id, date_of_purchase);
Then we put in some data:
INSERT INTO person VALUES (10, "jennifer", '2010-01-01');
INSERT INTO person VALUES (20, "dominick", '1978-01-01');
INSERT INTO audiobook VALUES (1234, 16, "harry potter");
Then we create the trigger:
CREATE TRIGGER check_purchases
AFTER INSERT ON audiobook_purchases
FOR EACH ROW
WHEN (
SELECT strftime('%Y', 'now') - strftime('%Y', date_of_birth) AS age
FROM person
WHERE new.customer_id=person.id) < (
SELECT audiobook.age_rating
FROM audiobook
WHERE audiobook.isbn=new.isbn)
BEGIN
DELETE FROM audiobook_purchases
WHERE isbn=new.isbn AND
customer_id=new.customer_id AND
date_of_purchase=new.date_of_purchase;
END;
I'll broke down the trigger into smaller steps:
AFTER INSERT ON audiobook_purchases creates a trigger on table audiobook_purchases which will be triggered after the insertion of a new record.
FOR EACH ROW applies the trigger to each new record inserted.
The WHEN clause limits triggering only to those records who satisfy its condition. On the left side of the < sign of the condition there is a query which selects the age of the customer. On the right side there is a query which selects the age rating of the book. Notice the reference to a new table. This table stores the record which triggers the event (see the two examples below). strftime is a scalar function which formats datetime stamps in SQLite. You can read:
strftime('%Y', 'now') as YEAR(NOW()) and
strftime('%Y', date_of_birth) as YEAR(date_of_birth).
Finally between BEGIN and END there are instructions that will be executed on triggering. In this case there is a single instruction which removes the record just inserted. MariaDb may provide a ROLLBACK statement, which can be more efficient than the DELETE statement.
So, for example:
INSERT INTO audiobook_purchases VALUES (1234, 10, '2018-11-25');
will activate the trigger, because the customer with id=10 ('jennifer') is 8 years old and the book with isbn=1234 requires the customer to be at least 16 years old, while:
INSERT INTO audiobook_purchases VALUES (1234, 20, '2018-11-25');
will not activate the trigger, because this customer is 40 years old.
You must be aware that this solution silently ignore the invalid order. I don't know if this is your desired behaviour.
I tested this trigger on SQLite 3.11.0, so it may not be compatible with your SQL interpreter.

Related

Oracle and mutating table with simple exercise

I'm in trouble with the implementation of a trigger.
Assuming that I have two types:
CREATE TYPE customer_t AS OBJECT(
code INTEGER,
name VARCHAR(20),
surname VARCHAR(20),
age INTEGER);
and the type
CREATE TYPE ticket_t AS OBJECT (
price INTEGER,
cust REF customer_t
)
And then I have the associate tables:
CREATE TABLE customers OF TYPE customer_t
CREATE TABLE tickets OF TYPE ticket_t
I have to do an exercise so I have to create a trigger for ensure that a customer won't buy more than 10 tickets but, if I use command like "select count(*)" I get an error because I can't access to mutating table.
Please can anyone help me with this trigger?
EDIT:
I populated the tables as follows:
INSERT INTO custs (code, name, surname, age) values (123, 'Paolo', 'Past', 32);
and repeating the following operation ten times:
INSERT INTO tickets (price, cust) values
(4, (SELECT * FROM (SELECT REF(T) FROM custs T WHERE name = 'Paolo' AND surname = 'Past') WHERE rownum < 2))
The trigger implemented is:
create or replace
trigger check_num_ticket after insert on tickets
for each row
declare
num_ticket number;
begin
SELECT count(*) INTO num_ticket FROM tickets WHERE :new.cust = cust;
if (num_ticket >= 10) then
raise_application_error('-20099', 'no ticket available');
end if;
end;
And I get this error:
A trigger (or a user defined plsql function that is referenced in
this statement) attempted to look at (or modify) a table that was
in the middle of being modified by the statement which fired it.
You are getting the mutating table error, because you are inserting in the same table where you want to get the row count for. Imagine your insert statement inserts two rows. There is no rule which row to insert first and which last, but your trigger fires on one inserted row and wants to know how many rows are already in the table. The DBMS tells you this is undefined, as the table is currently mutating.
You need an after statement trigger instead of a before row trigger. So when the insert statement's inserts are done, you look at the table to see whether there are suddenly customers with too many rows in it.
(A great alternative is a compound trigger. It combines row and statement triggers. So in the after row section you'd remember the customers in some array/collection and in the after statement section you'd look up the table for only the remembered customers.)

Attempting to add a new column in SQL by subtracting a year

Noob at this. So I will cut to the point on this. This is a class assignment and my professor has not answered emails since Thursday. The problem I am trying to solve is -
"Write a query that will subtract one year from the production date of each album and label this as the RecordDate and display the album title, production date, and record date for each entry in the table. Order the results by album title."
Here is what the table is supposed to look like
Here is the query I used (given in class I know repeated inserts but what he wanted)...
CREATE TABLE ALBUM(
ALBUM_ID char(4) UNIQUE NOT NULL,
ALBUM_TITLE varchar(255),
ALBUM_YEAR year,
ALBUM_PRODUCER varchar(255),
Primary Key(ALBUM_ID)
);
INSERT INTO ALBUM (ALBUM_ID, ALBUM_TITLE, ALBUM_YEAR, ALBUM_PRODUCER)
VALUES ('A001', 'Awake', '1994', 'East West Record');
INSERT INTO ALBUM (ALBUM_ID, ALBUM_TITLE, ALBUM_YEAR, ALBUM_PRODUCER)
VALUES ('A002', 'Moving Pictures', '1981', 'Anthem');
INSERT INTO ALBUM (ALBUM_ID, ALBUM_TITLE, ALBUM_YEAR, ALBUM_PRODUCER)
VALUES ('A003', 'Damage', '2013', 'RCA REcords');
INSERT INTO ALBUM (ALBUM_ID, ALBUM_TITLE, ALBUM_YEAR, ALBUM_PRODUCER)
VALUES ('A004', 'Continuum', '2006', 'Columbia Records');
Here is what I used to START to answer the question
ALTER TABLE ALBUM ADD RECORD_DATE INT;
UPDATE ALBUM SET RECORD_DATE=(ALBUM_YEAR-1);
This does make a new column and gives the results for what I want so far (have not gotten to the later part of the question). But this is two different queries...
So, from advice from SQL experts, to achieve what he wants will I have to write multiple queries or can this be done in one single query? Also are my datatypes okay?
No, I am not asking for the SQL to do this. This isn't a "PLEASE DO MY HOMEWORK FOR ME". And sorry, but it has to all be in cap locks because he wants it that way.
Andrew the tutor hasn't asked you to add a new column or anything, he has asked you to present data in a specific way.
We store data in SQL Server using some specific rules called Database Normalization rules.
But to show data in any specific form we write SELECT queries which select the data from the tables and we use all sorts of functions and methods to manipulate data at run-time and present the data in required structure/format/way.
Similarly in this case for your requirement you do not need to add another column just to do what your tutor as asked to do, it actually violates the rules of normalization. All you need is a simple SELECT query, which will show the data in the required format/way.
The select query will be something as simple as ....
SELECT [ALBUM_TITLE] AS [Album Title]
,[ALBUM_YEAR] AS [Production Date]
,[ALBUM_YEAR] - 1 AS [Record Date]
FROM ALBUM
ORDER BY [ALBUM_TITLE]
You don't need to alter the table at all. You're just going to want to select an additional column. I won't write the full query for you, but I will show you what I mean:
SELECT ALBUM_TITLE, ALBUM_YEAR, ALBUM_YEAR - 1 AS RECORD_DATE
-- rest of the query here --

Calculating age from birthday with oracle plsql trigger and insert the age in table

i have a table
dates
(dob date,
age number(4)
);
I will insert a date of birth and a trigger will calculate the age and insert that age in the age field.
CREATE OR REPLACE PROCEDURE getage IS
ndob date;
nage number(10);
BEGIN
select dob into ndob from dates;
select (sysdate-to_date(ndob))/365 into nage from dual;
update dates set age=nage;
END;
/
SHOW ERRORS;
this procedure works fine but for only one row, and I need trigger for all the rows but if I call it from a trigger then the error occurs.
CREATE OR REPLACE TRIGGER agec after INSERT OR UPDATE ON dates
FOR EACH ROW
BEGIN
getage;
END;
/
please help...i really need this...
No, you don't. I'm not sure you'll pay attention; and there's no reason why you should :-) but:
Do not store age in your database. You are absolutely guaranteed to be wrong occasionally. Age changes each year for each person, however, it changes every day for some people. This in turn means you need a batch job to run every day and update age. If this fails, or isn't extremely strict and gets run twice, you're in trouble.
You should always calculate the age when you need it. It's a fairly simple query and saves you a lot of pain in the longer run.
select floor(months_between(sysdate,<dob>)/12) from dual
I've set up a little SQL Fiddle to demonstrate
Now, to actually answer your question
this procedure works fine but for only one row,,,but for all the rows
i need trigger but if i call it from a trigger then the error
occurs...
You don't mention the error, please do this in future as it's very helpful, but I suspect you're getting
ORA-04091: table string.string is mutating, trigger/function may not
see it
This is because your procedure is querying the table that is being updated. Oracle does not allow this in order to maintain a read-consistent view of the data. The way to avoid this is to not query the table, which you don't need to do. Change your procedure to a function that returns the correct result given a date of birth:
function get_age (pDOB date) return number is
/* Return the the number of full years between
the date given and sysdate.
*/
begin
return floor(months_between(sysdate,pDOB)/12);
end;
Notice once again that I'm using the months_between() function as not all years have 365 days.
In your trigger you then assign the value directly to the column.
CREATE OR REPLACE TRIGGER agec before INSERT OR UPDATE ON dates
FOR EACH ROW
BEGIN
:new.age := get_age(:new.dob);
END;
The :new.<column> syntax is a reference to the <column> that is being updated. In this case :new.age is the actual value that is going to be put in the table.
This means that your table will automatically be updated, which is the point of a DML trigger.
As you can see there's little point to the function at all; your trigger can become
CREATE OR REPLACE TRIGGER agec before INSERT OR UPDATE ON dates
FOR EACH ROW
BEGIN
:new.age := floor(months_between(sysdate,:new,DOB)/12);
END;
However, having said that, if you are going to use this function elsewhere in the database then keep it separate. It's good practice to keep code that is used in multiple places in a function like this so it is always used in the same way. It also ensures that whenever anyone calculates age they'll do it properly.
As a little aside are you sure you want to allow people to be 9,999 years old? Or 0.000000000001998 (proof)? Numeric precision is based on the number of significant digits; this (according to Oracle) is non-zero numbers only. You can easily be caught out by this. The point of a database is to restrict the possible input values to only those that are valid. I'd seriously consider declaring your age column as number(3,0) to ensure that only "possible" values are included.
If you do it by a trigger the age is potentially wrong after one day. This is why saving the
age in a database table is bad practice and nobody does that. What you need is a view.
create view person_age as
select person_id,
floor(months_between(trunc(sysdate),birthdate)/12) as age
from birthdays;
Look at the SQL Fiddle
If you want AGE to be available from the database there are two options:
Define a view which contains the age computation, or
Define a virtual column which does the same
To define such a view:
CREATE OR REPLACE VIEW DATES_VIEW
AS SELECT DOB,
FLOOR(MONTHS_BETWEEN(SYSDATE, DOB) / 12) AS AGE
FROM DATES
The problem with this is that you have to remember that you SELECT from DATES_VIEW, but need to update DATES, which is mentally messy. IMO adding a virtual column to the table is cleaner:
CREATE TABLE DATES
(DOB DATE,
AGE GENERATED ALWAYS AS (FLOOR(MONTHS_BETWEEN(SYSDATE, DOB) / 12)) VIRTUAL);
Note that virtual columns cannot be updated.
Either method helps to ensure that AGE will always be consistent.
Share and enjoy.

SQLite create a new record if it doesn't exist

Hey so I'm trying to create a new record based on one which already exists, but I'm having trouble ensuring that this record doesn't already exist. The database stores details of transactions and has a field for whether it repeats.
I'm generating the new dates using
SELECT datetime(transactions.date, '+'||repeattransactions.interval||' days')
AS 'newdate' FROM transactions, repeattransactions WHERE
transactions.repeat = repeattransactions.id
Initially I'd like to use SELECT instead of INSERT just for debugging purposes. I've tried to do something with GROUP BY or COUNT(*) but the exact logic is eluding me. So for instance I've tried
SELECT * FROM transactions, (SELECT transactions.id, payee, category, amount,
fromaccount, repeat, datetime(transactions.date,
'+'||repeattransactions.interval||' days') AS 'date' FROM transactions,
repeattransactions WHERE transactions.repeat = repeattransactions.id)
but this obviously treats the two tables as though they were joined, it doesn't append the records to the bottom of the table which would let me group.
I've been struggling for days so any help would be appreciated!
EDIT:
The docs say "The ON CONFLICT clause applies to UNIQUE and NOT NULL constraints". I can't have a unique constraint on the table by definition as I have to create the exact same record for a new transaction with just a new date. Or have I misinterpreted this?
EDIT2:
id payee repeat date
1 jifoda 7 15/09/2011
2 jifoda 7 22/09/2011 <-- allowed as date different within subset
3 grefa 1 15/09/2011 <-- allowed as only date is not-unique
4 grefa 1 15/09/2011 <-- not allowed! exactly same as id 3
SQLite has special DDL statement for duplicates. Like:
create table test(a TEXT UNIQUE ON CONFLICT IGNORE);
insert into test values (1);
insert into test values (1);
select count(*) from test;
1
It can be also ON CONFLICT REPLACE. See docs.

Insert Data Into Tables Linked by Foreign Key

I am using PostgreSQL.
Customer
==================
Customer_ID | Name
Order
==============================
Order_ID | Customer_ID | Price
To insert an order, here is what I need to do usually,
For example, "John" place "1.34" priced order.
(1) Get Customer_ID from Customer table, where name is "John"
(2) If there are no Customer_ID returned (There is no John), insert "John"
(3) Get Customer_ID from Customer table, where name is "John"
(4) Insert "Customer_ID" and "1.34" into Order table.
There are 4 SQL communication with database involved for this simple operation!!!
Is there any better way, which can be achievable using 1 SQL statement?
You can do it in one sql statement for existing customers, 3 statements for new ones. All you have to do is be an optimist and act as though the customer already exists:
insert into "order" (customer_id, price) values \
((select customer_id from customer where name = 'John'), 12.34);
If the customer does not exist, you'll get an sql exception which text will be something like:
null value in column "customer_id" violates not-null constraint
(providing you made customer_id non-nullable, which I'm sure you did). When that exception occurs, insert the customer into the customer table and redo the insert into the order table:
insert into customer(name) values ('John');
insert into "order" (customer_id, price) values \
((select customer_id from customer where name = 'John'), 12.34);
Unless your business is growing at a rate that will make "where to put all the money" your only real problem, most of your inserts will be for existing customers. So, most of the time, the exception won't occur and you'll be done in one statement.
Not with a regular statement, no.
What you can do is wrap the functionality in a PL/pgsql function (or another language, but PL/pgsql seems to be the most appropriate for this), and then just call that function. That means it'll still be a single statement to your app.
Use stored procedures.
And even assuming you would want not to use stored procedures - there is at most 3 commands to be run, not 4. Second getting id is useless, as you can do "INSERT INTO ... RETURNING".