Can I block an insert inside a trigger on Oracle 11g? - sql

So, what I want to do is to block an insert if the condition is met. In this case, I'm developing a simple library database system and I don't want to allow someone to be able to borrow a book if that certain person has already borrowed 2 books and still hasn't returned any of them.
I need to do this via trigger, is it possible? Also, I'd like to know if I can supply a query into the WHEN clause like I did.
CREATE OR REPLACE TRIGGER trigger_borrowing_limit
BEFORE INSERT ON Borrowing
REFERENCING NEW ROW AS NROW
FOR EACH ROW
WHEN SELECT COUNT(*) FROM Borrowing WHERE RETURN_DATE IS NULL AND NROW.ID = Borrowing.ID > 2
BEGIN
?
END

You can't supply a query for the WHEN clause. From the Oracle 11 docs (search for "WHEN (condition)" once you get to the page):
Restrictions on WHEN (condition)
If you specify this clause, then you must also specify FOR EACH ROW.
The condition cannot include a subquery or a PL/SQL expression (for example, an invocation of a user-defined function).
As for preventing the row insert, the comment from Mihai is the way to go. Your front end can catch the exception, and if it's error number -20101 (per Mihai's example) you'll know that the person already has two books out. Note that some drivers will report the absolute value of the exception number, so the error number that reaches you may be 20101.
Addendum: a followup question asked how to apply the "two books out" logic since it's not valid for the WHEN clause.
The answer is to drop the WHEN clause and put the logic into the trigger body. Note that I normally stick with the standard NEW for referencing the new row, so my answer doesn't have the REFERENCING NEW AS NROW:
CREATE OR REPLACE TRIGGER trigger_borrowing_limit
BEFORE INSERT ON Borrowing
DECLARE
booksOut NUMBER;
BEGIN
SELECT COUNT(*) INTO booksOut
FROM Borrowing
WHERE RETURN_DATE IS NULL AND NEW.ID = Borrowing.ID;
IF booksOut > 2 THEN
-- Next line courtesy of Mihai's comment under the question
raise_application_error(-20101, 'You already borrowed 2 books');
END IF;
END;

Related

Oracle Trigger Error : missing left parenthesis

can anyone help me out for this trigger.
CREATE or replace trigger check_limit_to_Y
AFTER INSERT OR UPDATE ON api_user for each row
WHEN EXISTS (SELECT '1' FROM profile b WHERE NEW.mvno_limit!='Y' and b.mvno_id = NEW.mvno_id)
BEGIN
raise_application_error (-20999,'MVNO LIMIT MUST BE SET Y FOR ANY REAL MVNO_ID');
END;
I got the error
Error report -
ORA-00906: missing left parenthesis
00906. 00000 - "missing left parenthesis"
*Cause:
*Action:
This is from the Oracle site: https://docs.oracle.com/cd/E11882_01/appdev.112/e25519/create_trigger.htm#LNPLS01374
WHEN (condition)
Specifies a SQL condition that the database evaluates for each row that the triggering statement affects. If the value of condition is TRUE for an affected row, then tps_body runs for that row; otherwise, tps_body does not run for that row. The triggering statement runs regardless of the value of condition.
The condition can contain correlation names (see referencing_clause ::=). In condition, do not put a colon (:) before the correlation name NEW, OLD, or PARENT (in this context, it is not a placeholder for a bind variable).
See Also:
Oracle Database SQL Language Reference for information about SQL conditions
Restrictions on WHEN (condition)
If you specify this clause, then you must also specify at least one of these timing points:
BEFORE EACH ROW
AFTER EACH ROW
INSTEAD OF EACH ROW
The condition cannot include a subquery or a PL/SQL expression (for example, an invocation of a user-defined function).
Your code doesn't seem to satisfy the last two restrictions (also: the entire condition after the WHEN keyword must be enclosed in parenthesis).
But, you seem to need to use the 'compound trigger syntax' because you want to process both inserts and updates. Check the documentation carefully.
Update: Try this:
CREATE TRIGGER t
BEFORE INSERT OR UPDATE ON api_user FOR EACH ROW
DECLARE CNT integer;
BEGIN
SELECT count(*) into CNT FROM profile b where :NEW.mvno_limit!='Y' and b.mvno_id = :NEW.mvno_id;
IF CNT > 0 THEN
RAISE_APPLICATION_ERROR(-20999,'MVNO LIMIT MUST BE SET Y FOR ANY REAL MVNO_ID');
END IF;
END;

Postgres ATOMIC stored procedure INSERT INTO . . . SELECT with one parameter and one set of rows from a table

I am trying to write a stored procedure to let a dev assign new user identities to a specified group when they don't already have one (i.e. insert a parameter and the output of a select statement into a joining table) without hand-writing every pair of foreign keys as values to do so. I know how I'd do it in T-SQL/SQL Server but I'm working with a preexisting/unfamiliar Postgres database. I would strongly prefer to keep my stored procedures as LANGUAGE SQL/BEGIN ATOMIC and this + online examples being simplified and/or using constants has made it difficult for me to get my bearings.
Apologies in advance for length, this is me trying to articulate why I do not believe this question is a duplicate based on what I've been able to find searching on my own but I may have overcorrected.
Schema (abstracted from the most identifying parts; these are not the original table names and I am not in a position to change what anything is called; I am also leaving out indexing for simplicity's sake) is like:
create table IF NOT EXISTS user_identities (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY NOT NULL,
[more columns not relevant to this query)
)
create table IF NOT EXISTS user_groups (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY NOT NULL,
name TEXT NOT NULL
)
create table IF NOT EXISTS group_identities (
user_id BIGINT REFERENCES user_identities(id) ON DELETE RESTRICT NOT NULL,
group_id BIGINT REFERENCES user_groups(id) ON DELETE RESTRICT NOT NULL
)
Expected dev behavior:
Add all predetermined identities intended to belong to a group in a single batch
Add identifying information for the new group (it is going to take a lot of convincing to bring the people involved around to using nested stored procedures for this if I ever can)
Bring the joining table up to date accordingly (what I've been asked to streamline).
If this were SQL Server I would do (error handling omitted for time and putting aside whether EXCEPT or NOT IN would be best for now, please)
create OR alter proc add_identities_to_group
#group_name varchar(50) NULL
as BEGIN
declare #use_group_id int
if #group_name is NULL
set #use_group_id = (select Top 1 id from user_groups where id not in (select group_id from group_identities) order by id asc)
ELSE set #use_group_id = (select id from user_groups where name = #group_name)
insert into group_identities (user_id, group_id)
select #use_group_id, id from user_identities
where id not in (select user_id from group_identities)
END
GO
Obviously this is not going to fly in Postgres; part of why I want to stick with atomic stored procedures is staying in "neutral" SQL, both to be closer to my comfort zone and because I don't know what other languages the database is currently set up for, but my existing education has played kind of fast and loose with differentiating what was T-SQL specific at any point.
I am aware that this is not going to run for a wide variety of reasons because I'm still trying to internalize the syntax, but the bad/conceptual draft I have written so that I have anything to stare at is:
create OR replace procedure add_identities_to_groups(
group_name text default NULL ) language SQL
BEGIN ATOMIC
declare use_group_id integer
if group_name is NULL
set use_group_id = (select Top 1 id from user_groups
where id not in (select user_id from group_identities)
order by id asc)
ELSE set use_group_id = (select id from user_groups where name = group_name) ;
insert into group_identities (group_id, user_id)
select use_group_id, id from user_identities
where id not in (select user_id from group_identities)
END ;
GO ;
Issues:
Have not found either answers for how to do this with the combination of a single variable and a column with BEGIN ATOMIC or hard confirmation that it wouldn't work (e.g. can atomic stored procedures just not accept parameters? I cannot find an answer to this on my own). (This is part of why existing answers that I can find here and elsewhere haven't been clarifying for me.)
~~Don't know how to compensate for Postgres's not differentiating variables and parameters from column names at all. (This is why examples using a hardcoded constant haven't helped, and they make up virtually all of what I can find off StackOverflow itself.)~~ Not a problem if Postgres will handle that intelligently within the atomic block but that's one of the things I hadn't been able to confirm on my own.
Google results for "vanilla" SQL unpredictably saturated with SQL Server anyway, while my lack of familiarity with Postgres is not doing me any favors but I don't know anyone personally who has more experience than I do.
because I don't know what other languages the database is currently set up for
All supported Postgres versions always include PL/pgSQL.
If you want to use procedural elements like variables or conditional statements like IF you need PL/pgSQL. So your procedure has to be defined with language plpgsql - that removes the possibility to use the ANSI standard BEGIN ATOMIC syntax.
Don't know how to compensate for Postgres's not differentiating variables and parameters from column names at all.
You don't. Most people simply using naming conventions to do that. In my environment we use p_ for parameters and l_ for "local" variables. Use whatever you prefer.
Quote from the manual
By default, PL/pgSQL will report an error if a name in an SQL statement could refer to either a variable or a table column. You can fix such a problem by renaming the variable or column, or by qualifying the ambiguous reference, or by telling PL/pgSQL which interpretation to prefer.
The simplest solution is to rename the variable or column. A common coding rule is to use a different naming convention for PL/pgSQL variables than you use for column names. For example, if you consistently name function variables v_something while none of your column names start with v_, no conflicts will occur.
As documented in the manual the body for a procedure written in PL/pgSQL (or any other language that is not SQL) must be provided as a string. This is typically done using dollar quoting to make writing the source easier.
As documented in the manual, if you want to store the result of a single row query in a variable, use select ... into from ....
As documented in the manual an IF statement needs a THEN
As documented in the manual there is no TOP clause in Postgres (or standard SQL). Use limit or the standard compliant fetch first 1 rows only instead.
To avoid a clash between names of variables and column names, most people use some kind of prefix for parameters and variables. This also helps to identify them in the code.
In Postgres it's usually faster to use NOT EXISTS instead of NOT IN.
In Postgres statements are terminated with ;. GO isn't a SQL command in SQL Server either - it's a client side thing supported by SSMS. To my knowledge, there is no SQL tool that works with Postgres that supports the GO "batch terminator" the same way SSMS does.
So a direct translation of your T-SQL code to PL/pgSQL could look like this:
create or replace procedure add_identities_to_groups(p_group_name text default NULL)
language plpgsql
as
$$ --<< start of PL/pgSQL code
declare --<< start a block for all variables
l_use_group_id integer;
begin --<< start the actual code
if p_group_name is NULL THEN --<< then required
select id
into l_use_group_id
from user_groups ug
where not exists (select * from group_identities gi where gi.id = ug.user_id)
order by ug.id asc
limit 1;
ELSE
select id
into l_use_group_id
from user_groups
where name = p_group_name;
end if;
insert into group_identities (group_id, user_id)
select l_use_group_id, id
from user_identities ui
where not exists (select * from group_identities gi where gi.user_id = ui.id);
END;
$$
;

How to use a temp sequence within a Postgresql function

I have some lines of SQL which will take a set of IDs from the same GROUP_ID that are not contiguous (ex. if some rows got deleted) and will make them contiguous again. I wanted to turn this into a function for reusability purposes. The lines work if executed individually but when I try to create the function I get the error
ERROR: relation "id_seq_temp" does not exist
LINE 10: UPDATE THINGS SET ID=nextval('id_se...
If I create a sequence outside of the function and use that sequence in the function instead then the function is created successfully (schema qualified or unqualified). However I felt like creating the temp sequence inside of the function rather than leaving it in the schema was a cleaner solution.
I have seen this question: Function shows error "relation my_table does not exist"
However, I'm using the public schema and schema qualifying the sequence with public. does not seem to help.
I've also seen this question: How to create a sql function using temp sequences and a SELECT on PostgreSQL8. I probably could use generate_series but this adds a lot of complexity that SERIES solves such as needing to know how big of a series to generate.
Here is my function, I anonymized some of the names - just in case there's a typo.
CREATE OR REPLACE FUNCTION reindex_ids(IN BIGINT) RETURNS VOID
LANGUAGE SQL
AS $$
CREATE TEMPORARY SEQUENCE id_seq_temp
MINVALUE 1
START WITH 1
INCREMENT BY 1;
ALTER SEQUENCE id_seq_temp RESTART;
UPDATE THINGS SET ID=ID+2000 WHERE GROUP_ID=$1;
UPDATE THINGS SET ID=nextval('id_seq_temp') WHERE GROUP_ID=$1;
$$;
Is it possible to use a sequence you create within a function later in the function?
Answer to question
The reason is that SQL functions (LANGUAGE sql) are parsed and planned as one. All objects used must exist before the function runs.
You can switch to PL/pgSQL, (LANGUAGE plpgsql) which plans each statement on demand. There you can create objects and use them in the next command.
See:
Why can PL/pgSQL functions have side effect, while SQL functions can't?
Since you are not returning anything, consider a PROCEDURE. (FUNCTION works, too.)
CREATE OR REPLACE PROCEDURE reindex_ids(IN bigint)
LANGUAGE plpgsql AS
$proc$
BEGIN
IF EXISTS ( SELECT FROM pg_catalog.pg_class
WHERE relname = 'id_seq_temp'
AND relnamespace = pg_my_temp_schema()
AND relkind = 'S') THEN
ALTER SEQUENCE id_seq_temp RESTART;
ELSE
CREATE TEMP SEQUENCE id_seq_temp;
END IF;
UPDATE things SET id = id + 2000 WHERE group_id = $1;
UPDATE things SET id = nextval('id_seq_temp') WHERE group_id = $1;
END
$proc$;
Call:
CALL reindex_ids(123);
This creates your temp sequence if it does not exist already.
If the sequence exists, it is reset. (Remember that temporary objects live for the duration of a session.)
In the unlikely event that some other object occupies the name, an exception is raised.
Alternative solutions
Solution 1
This usually works:
UPDATE things t
SET id = t1.new_id
FROM (
SELECT pk_id, row_number() OVER (ORDER BY id) AS new_id
FROM things
WHERE group_id = $1 -- your input here
) t1
WHERE t.pk_id = t1.pk_id;
And only updates each row once, so half the cost.
Replace pk_id with your PRIMARY KEY column, or any UNIQUE NOT NULL (combination of) column(s).
The trick is that the UPDATE typically processes rows according to the sort order of the subquery in the FROM clause. Updating in ascending order should never hit a duplicate key violation.
And the ORDER BY clause of the window function row_number() imposes that sort order on the resulting set. That's an undocumented implementation detail, so you might want to add an explicit ORDER BY to the subquery. But since the behavior of UPDATE is undocumented anyway, it still depends on an implementation detail.
You can wrap that into a plain SQL function.
Solution 2
Consider not doing what you are doing at all. Gaps in sequential numbers are typically expected and not a problem. Just live with it. See:
Serial numbers per group of rows for compound key

Sql Trigger Trouble

Hey guys i cant get this trigger to work, ive worked on it for an hour or so and cant see to figure out where im going wrong, any help would be appreciated
CREATE OR REPLACE TRIGGER allergy
BEFORE INSERT ON
DECLARE
med VARCHAR2(20);
BEGIN
SELECT v.medication RCD.specify
INTO med
FROM visit v, relcondetails RCD
WHERE :new.medication = v.medication AND RCD.specifiy = 'allergies';
IF med = allergies THEN
RAISE_APPLICATION_ERROR(-20000, 'Patient Is alergic to this medication');
END IF;
END allergy;
When put into oracle
ERROR at line 6: ORA-04079: invalid
trigger specification
CREATE OR REPLACE TRIGGER allergy BEFORE INSERT ON
name of table here
FOR EACH ROW -- forgot this too
DECLARE
med VARCHAR2(20);
You should really be declaring this as %type.
med visit.medication%type;
BEGIN
SELECT v.medication RCD.specify
Requires a comma between columns
INTO med
Two columns need two variables
FROM visit v, relcondetails RCD
WHERE :new.medication = v.medication AND RCD.specifiy = 'allergies';
You have no join condition between your two tables, that's very bad. This query will perform a Cartesian between the two tables and then return all of them that have 'allergies' and :new.medication in their respective columns.
you also probably need a filter condition to limit the query to a particular patient or a particular visit. This query will do it for all patients and all their visits squared.
IF med = allergies THEN
I don't know what /allergies/ is in this IF. There's no variable that's defined as that and without quotes it's not a string.
RAISE_APPLICATION_ERROR(-20000, 'Patient Is alergic to this medication');
This error message reinforces what I said about your query. You think you're querying for a single patient but you're not.
END IF;
END allergy;
Seriously, if you're writing software to save a person from getting potentially life threatening medication then please consider some other line of work. I swear I'm not saying this to be rude, but your code sample shows almost no understanding of the pl/sql language or sql or any scrap of programming background. I think you started with some sample code and tried to modify it into something something. But you're really left with gibberish. I'm starting to think this is homework.
In addition to Mark's point that you are missing the table name, and Martin's point that you want this to be a row-level trigger, your actual body won't compile, for a couple of reasons.
You look like you're trying to select two columns, but you don't have a comma between them, and you only have one local variable in the INTO clause
You use an identifier allergies which is not declared anywhere.
I also doubt that your query is logically correct, but of course I don't know the database design so I can't say for sure.
BEFORE INSERT ON <TABLE NAME>
and why select both v.medication and RCD.specify when you're only selecting into one variable?
You've probably seen this but:
http://msdn.microsoft.com/en-us/library/aa258254(SQL.80).aspx
CREATE TRIGGER TriggerName
ON MyTableName
FOR MyEvent
AS
-- My Trigger Logic

MySQL - Set default value for field as a string concatenation function

I have a table that looks a bit like this actors(forename, surname, stage_name);
I want to update stage_name to have a default value of
forename." ".surname
So that
insert into actors(forename, surname) values ('Stack', 'Overflow');
would produce the record
'Stack' 'Overflow' 'Stack Overflow'
Is this possible?
Thanks :)
MySQL does not support computed columns or expressions in the DEFAULT option of a column definition.
You can do this in a trigger (MySQL 5.0 or greater required):
CREATE TRIGGER format_stage_name
BEFORE INSERT ON actors
FOR EACH ROW
BEGIN
SET NEW.stage_name = CONCAT(NEW.forename, ' ', NEW.surname);
END
You may also want to create a similar trigger BEFORE UPDATE.
Watch out for NULL in forename and surname, because concat of a NULL with any other string produces a NULL. Use COALESCE() on each column or on the concatenated string as appropriate.
edit: The following example sets stage_name only if it's NULL. Otherwise you can specify the stage_name in your INSERT statement, and it'll be preserved.
CREATE TRIGGER format_stage_name
BEFORE INSERT ON actors
FOR EACH ROW
BEGIN
IF (NEW.stage_name IS NULL) THEN
SET NEW.stage_name = CONCAT(NEW.forename, ' ', NEW.surname);
END IF;
END
According to 10.1.4. Data Type Default Values no, you can't do that. You can only use a constant or CURRENT_TIMESTAMP.
OTOH if you're pretty up-to-date, you could probably use a trigger to accomplish the same thing.
My first thought is if you have the two values in other fields what is the compelling need for redundantly storing them in a third field? It flies in the face of normalization and efficiency.
If you simply want to store the concatenated value then you can simply create a view (or IMSNHO even better a stored procedure) that concatenates the values into a pseudo actor field and perform your reads from the view/sproc instead of the table directly.
If you absolutely must store the concatenated value you could handle this in two ways:
1) Use a stored procedure to do your inserts instead of straight SQL. This way you can receive the values and construct a value for the field you wish to populate then build the insert statement including a concatenated value for the actors field.
2) So I don't draw too many flames, treat this suggestion with kid gloves. Use only as a last resort. You could hack this behavior by adding a trigger to build the value if it is left null. Generally, triggers are not good. They add unseen cost and interactions to fairly simple interactions. You can, though, use the CREATE TRIGGER to update the actors field after a record is inserted or updated. Here is the reference page.
As of MySQL 8.0.13, you can use DEFAULT clause for a column which can be a literal constant or an expression.
If you want to use an expression then, simply enclose the required expression within parentheses.
(concat(forename," ",surname))
There are two ways to accomplish what you are trying to do as per my knowledge:
(important: consider backing up your table first before running below queries)
1- Drop the column "stage_name" all together and create a new one with DEFAULT constraint.
ALTER TABLE actors ADD COLUMN stage_name VARCHAR(20) DEFAULT (concat(forename," ",surname))
2- This will update newer entries in the column "stage_name" but not the old ones.
ALTER TABLE actors alter stage_name set DEFAULT (concat(forename," ",surname));
After that, if you need to update the previous values in the column "stage_name" then simply run:
UPDATE actors SET stage_name=(concat(forename," ",surname));
I believe this should solve your problem.