Is an insert and a corresponding trigger call an atomic process? - sql

I have a table called LOCK and I want to ensure that not more than a single row with a given name and type WRITE exists. Though multiple rows with type READ and an equal name are allowed but only if there is no row with the same name and type WRITE.
create table "LOCK"
(
"LOCK_ID" NUMBER(19,0) NOT NULL,
"NAME" VARCHAR2(255 CHAR),
"TYPE" VARCHAR2(32 CHAR),
CONSTRAINT "SYS_LOCK_PK" PRIMARY KEY ("LOCK_ID")
);
Inserting a row has to be atomic, for instance no query with a following insert depending on the result of the query (because it could have changed meanwhile).
To ensure atomicy I created a trigger to check the initially mentioned condition (raising error on fail), which is occasionally ending up in various invalid states like two WRITE rows.
If inserts are executed sequentially the trigger works perfectly which leads to the assumption insert + trigger is no atomic process and if so, is there a safe way to solve my issue?
Here's the trigger:
create or replace trigger "LOCK_TRIGGER"
before insert on "LOCK"
referencing NEW AS NEW
for each row
declare
c integer := 0;
begin
select count(*) into c from "LOCK" where (:NEW.typ = 'WRITE' and name = :NEW.name) or (:NEW.typ = 'READ' and name = :NEW.name and typ = 'WRITE');
if (c > 0) then
raise_application_error(-20634, 'Nope!');
end if;
end;

Trigger doesn't help here for the multiuser environment. You need to serialize the access to the particular lock name. For this case I would go for the custom locks. The database package dbms_lock is used for this. You can create a function which does the following:
acquires custom lock for the incoming name - this lock should be created with an option that it is not released on commit/rollback
makes the validation in the table for the name
inserts the record if possible (if validation passed) and commits it
releases custom lock
returns the result (either OK or NOK)
Hope that helps.

Related

UPSERT inserts duplicate null entry into table (ORACLE)

I am trying to make an upsert trigger on ORACLE via PL/SQL by checking some examples, i am doing fine, i think it is the last step i should only configure. My requirement is that :
A system that will insert to that field will remain one column always null, so i will read column value from another table, then upsert it with inclusion of that value.
d2c_region_locale_config holds d2c_is_active value, so i firstly read that value regarding to locale condition then trigger inserts or updates to table with addition of this value on active_for_d2c column.(for update i am using locale and country columns as it is shown on where clause, they are not PK but has not null condition)
So i've created this trigger:
CREATE OR REPLACE TRIGGER BL_PIM_LOCALE_COUNTRY
BEFORE INSERT OR UPDATE ON PIM_LOCALE_COUNTRY REFERENCING NEW AS NEW OLD AS OLD
FOR EACH ROW
DECLARE
l_active_for_d2c INTEGER;
BEGIN
if :NEW.active_for_d2c is null then
DELETE from pim_locale_country where active_for_d2c is null;
select distinct(d2c_isactive) into l_active_for_d2c from d2c_region_locale_config where d2c_locale= :NEW.locale;
UPDATE pim_locale_country
SET locale = :NEW.locale, locale_name = :NEW.locale_name,
country = :NEW.country, country_name = :NEW.country_name, isdummy = :NEW.isdummy,
active_for_d2c = l_active_for_d2c, itextpos = :NEW.itextpos, locale_charset = :NEW.locale_charset,
fallback_locale = :NEW.fallback_locale, default_for_lang = :NEW.default_for_lang, opeclang = :NEW.opeclang
where locale = :NEW.locale and country = :NEW.country;
IF ( sql%notfound ) THEN
INSERT INTO PIM_LOCALE_COUNTRY (locale,locale_name,country,country_name,isdummy,active_for_d2c,itextpos,locale_charset,fallback_locale,default_for_lang,opeclang)
VALUES (:NEW.locale, :NEW.locale_name,:NEW.country,:NEW.country_name,:NEW.isdummy,l_active_for_d2c,:NEW.itextpos,:NEW.locale_charset,:NEW.fallback_locale,:NEW.default_for_lang,:NEW.opeclang);
END IF;
end if;
END;
It currently does the job, reads value and inserts or updates the existing locale-country couple for other values. But critical thing is that, table always has one "null" value(Please check screenshot), even that i run delete statement at the beginning on my trigger. So my question would be how to delete, or how to make this approach on trigger side ?
Many thanks for answers!
Trigger before insert doesn't block insert itself, so you insert that record twice. That is, once your trigger done its work (inserted or updated record), oracle will proceed with insert (or update) using values that stand in NEW record of your trigger. If trigger modifies NEW., it will be stored as you changed it, but if trigger inserts something itself, you can get more records.
You can use instead of insert or update triggers, and then oracle will not run its own inserts/updates after trigger finishes.
But more common way for 1-record triggers is to modify fields in NEW, for this case field NEW.d2c_is_active.
It looks like this (possible typos, please check)
CREATE OR REPLACE TRIGGER BL_PIM_LOCALE_COUNTRY
BEFORE INSERT OR UPDATE ON PIM_LOCALE_COUNTRY REFERENCING NEW AS NEW OLD AS OLD
FOR EACH ROW
BEGIN
if :NEW.active_for_d2c is null then
select d2c_isactive
into :NEW.active_for_d2c
from d2c_region_locale_config
where d2c_locale= :NEW.locale and rownum<=1;
end if;
END;

How to add constraint to sql table so that table has exactly one row

Parameter table is initially created and one row is added in Postgres.
This table should have always one row, otherwise SQL queries using this table will produce incorrect results. DELETE or INSERT to this table are disallowed, only UPDATE is allowed.
How to add single row constraint to this table?
Maybe DELETE and INSERT triggers can raise an exception or is there simpler way?
The following will create a table where you can only insert one single row. Any update of the id column will result in an error, as will any insert with a different value than 42. The actual id value doesn't matter actually (unless there is some special meaning that you need).
create table singleton
(
id integer not null primary key default 42,
parameter_1 text,
parameter_2 text,
constraint only_one_row check (id = 42)
);
insert into singleton values (default);
To prevent deletes you can use a rule:
create or replace rule ignore_delete
AS on delete to singleton
do instead nothing;
You could also use a rule to make insert do nothing as well if you want to make an insert "fail" silently. Without the rule, an insert would generate an error. If you want a delete to generate an error as well, you would need to create a trigger that simply raises an exception.
Edit
If you want an error to be thrown for inserts or deletes, you need a trigger for that:
create table singleton
(
id integer not null primary key,
parameter_1 text,
parameter_2 text
);
insert into singleton (id) values (42);
create or replace function raise_error()
returns trigger
as
$body$
begin
RAISE EXCEPTION 'No changes allowed';
end;
$body$
language plpgsql;
create trigger singleton_trg
before insert or delete on singleton
for each statement execute procedure raise_error();
Note that you have to insert the single row before you create the trigger, otherwise you can't insert that row.
This will only partially work for a superuser or the owner of the table. Both have the privilege to drop or disable the trigger. But that is the nature of a superuser - he can do anything.
To make any table a singleton just add this column:
just_me bool NOT NULL DEFAULT TRUE UNIQUE CHECK (just_me)
This allows exactly one row. Plus add the trigger #a_horse provided.
But I would rather use a function instead of the table for this purpose. Simpler and cheaper.
CREATE OR REPLACE FUNCTION one_row()
RETURNS TABLE (company_id int, company text) LANGUAGE sql IMMUTABLE AS
$$SELECT 123, 'The Company'$$
ALTER FUNCTION one_row() OWNER TO postgres;
Set the owner to the user that should be allowed to change it.
Give a user permission to ALTER a function
Nobody else change it - except superusers of course. Superusers can do anything.
You can use this function just like you would use the table:
SELECT * FROM one_row();
If you need a "table", create a view (which is actually a special table internally):
CREATE VIEW one_row AS SELECT * FROM one_row();
I guess you will not use the PostgreSQL root user in your application so you could simply limit the permissions of your application user on UPDATE for this table.
An INSERT or DELETE will then cause an Insufficient privilege exception.

Oracle DBMS - Read a table before processing Updating in an AFTER trigger - mutating table

I've been experimenting with Oracle over the past few weeks and I've stumbled upon an issue which I can't seem to wrap my head around.
I am building a small property management system and I am trying to process as many actions as possible on the database side (purely experimental, I just wanted to clear this up before anyone asks, "why dont you just update these rows through the client")
In my system, I have a properties and rooms table (simplified schema below):
`-------------------------------
` PROPERTIES
`-------------------------------
`- PropertyID: PK
`- PropertyStatus: VARCHAR
`-------------------------------
`-------------------------------
` ROOMS
`-------------------------------
`- RoomID: PK
`- PropertyID: FK
`- RoomStatus: VARCHAR
`-------------------------------
Whenever a user is assigned to a room the rooms status is updated to OCCUPIED, once this happens I wish to check how many rooms associated with property n are taken, if all rooms are taken the property_status should be Updated to FULL, then if users are unassigned from properties the value updates to VACANCIES AVAILABLE etc.
I have the basic logic for this mapped out:
-- Return how many vacant rooms belong to this property
CREATE OR REPLACE FUNCTION prop_vacancy_query(
p_property_id properties.property_id%TYPE
)
RETURN NUMBER
IS
v_prop_rooms NUMBER;
BEGIN
SELECT COUNT(room_status)
INTO v_prop_rooms
FROM rooms
JOIN properties ON
rooms.property_id = properties.property_id
WHERE room_status = 'VACANT'
AND rooms.property_id = p_property_id;
RETURN v_prop_rooms;
END prop_vacancy_query;
In my AFTER trigger on my rooms table I try to call the query but I get a mutating table error, I believe this is because prop_vacancy_query is reading the properties table.
CREATE OR REPLACE TRIGGER trg_rooms_after
AFTER INSERT OR UPDATE ON rooms FOR EACH ROW
BEGIN
-- Update the table based on the result
IF prop_vacancy_query(:NEW.property_id) = 0 THEN
UPDATE properties
SET prop_status = 'VACANT'
WHERE properties.property_id = :NEW.property_id;
ELSE
UPDATE properties
SET prop_status = 'FULL'
WHERE properties.property_id = :NEW.property_id;
END IF;
END;
Previously this code worked for my system, but since reading more into pragma autonomous transactions I have realised it was extremely bad practice to run the prop_vacancy_query() on its own independent transaction.
Is there any way that I can read from the properties table and then update the rooms table without getting a mutating error?
Just to clarify, the mutating table exception is thrown because you are trying to read from the rooms table in your function, not because you are trying to read from the properties table. Since you have a row-level trigger on rooms, that means that the rooms table is in the middle of a change when the row-level trigger is firing and that it may be in an inconsistent state. Oracle prevents you from querying the rooms table in that situation because the results are not necessarily deterministic or reproducible.
If you created a statement-level trigger (removing the FOR EACH ROW) and put your logic there, you would no longer encounter a mutating table exception because the rooms table would no longer be in an inconsistent state. A statement-level trigger, though, is not able to see which row(s) were modified. That would mean that you'd need to look across all properties to see which status values should be adjusted. That's not going to be particularly efficient.
At the cost of additional complexity, you can improve the performance by capturing which properties changed in a row-level trigger and then referring to that in a statement-level trigger. That generally requires three triggers and a package, which obviously increases the number of moving pieces substantially (if you're on 11.2, you can use a compound trigger with three component triggers which simplifies things a bit by eliminating the need to use the package). That would look something like
CREATE OR REPLACE PACKAGE trigger_collections
AS
TYPE modified_property_tbl IS TABLE OF properties.property_id%type;
g_modified_properties modified_property_tbl;
END;
-- Initialize the collection in a before statement trigger just in case
-- there were values there from a prior run
CREATE OR REPLACE TRIGGER trg_initialize_mod_prop_coll
BEFORE INSERT OR UPDATE ON rooms
BEGIN
trigger_collections.g_modified_properties := trigger_collections.modified_property_tbl();
END;
-- Put the property_id of the modified row in the collection
CREATE OR REPLACE TRIGGER trg_populate_mod_prop_coll
AFTER INSERT OR UPDATE ON rooms
FOR EACH ROW
BEGIN
trigger_collections.g_modified_properties.extend();
trigger_collections.g_modified_properties( trigger_collections.g_modified_properties.count + 1 ) := :new.property_id;
END;
CREATE OR REPLACE TRIGGER trg_process_mod_prop_coll
AFTER INSERT OR UPDATE ON rooms
BEGIN
FOR p IN 1 .. trigger_collections.g_modified_properties.count
LOOP
IF prop_vacancy_query( trigger_collections.g_modified_properties(i) ) = 0
THEN
...
END;

Compound trigger: collecting mutating rows into nested table

I have two tables in my project: accounts and transactions (one-to-many relationship). In every transaction I store the balance of the associated account (after the transaction is executed). Additionally in every transaction I store a value of the transaction.
So I needed a trigger fired when someone adds new transaction. It should check whether new account balance will be correct (old account balance + transaction value = new account balance stored in transaction).
So I was suggested, I should use a compound trigger which would:
in before each row section: save a row's PK (made of two columns) somewhere,
in after statement section: check whether all inserted transactions where correct.
Now I can't find anywhere how could I implement the first point.
What I already have:
CREATE OR REPLACE TRIGGER check_account_balance_is_valid
FOR INSERT
ON Transactions
COMPOUND TRIGGER
TYPE Modified_transactions_T IS TABLE OF Transactions%ROWTYPE;
Modified_transactions Modified_transactions_T;
BEFORE STATEMENT IS BEGIN
Modified_transactions := Modified_transactions_T();
END BEFORE STATEMENT;
BEFORE EACH ROW IS BEGIN
Modified_transactions.extend;
Modified_transactions(Modified_transactions.last) := :NEW;
END BEFORE EACH ROW;
AFTER STATEMENT IS BEGIN
NULL; -- I will write something here later
END AFTER STATEMENT;
END check_account_balance_is_valid;
/
However, I got that:
Warning: execution completed with warning
11/58 PLS-00049: bad bind variable 'NEW'
Could someone tell me, how to fix it? Or maybe my whole "compound trigger" idea is wrong and you have better suggestions.
Update 1
Here is my ddl script: http://pastebin.com/MW0Eqf9J
Maybe try this one:
TYPE Modified_transactions_T IS TABLE OF ROWID;
Modified_transactions Modified_transactions_T;
BEFORE STATEMENT IS BEGIN
Modified_transactions := Modified_transactions_T();
END BEFORE STATEMENT;
BEFORE EACH ROW IS BEGIN
Modified_transactions.extend;
Modified_transactions(Modified_transactions.last) := :NEW.ROWID;
END BEFORE EACH ROW;
or this
TYPE PrimaryKeyRecType IS RECORD (
Col1 Transactions.PK_COL_1%TYPE, Col2 Transactions.PK_COL_2%TYPE);
TYPE Modified_transactions_T IS TABLE OF PrimaryKeyRecType;
...
Modified_transactions(Modified_transactions.last) := PrimaryKeyRecType(:NEW.PK_COL_1, :NEW.PK_COL_2);
Your immediate problem is that :new is not a real record so it is not of type Transactions%ROWTYPE. If you're really going to go down this path, you would generally want to declare a collection of the primary key of the table
TYPE Modified_transactions_T IS TABLE OF Transactions.Primary_Key%TYPE;
and then put just the primary key in the collection
BEFORE EACH ROW IS BEGIN
Modified_transactions.extend;
Modified_transactions(Modified_transactions.last) := :NEW.Primary_Key;
END BEFORE EACH ROW;
The fact that you are trying to work around a mutating table exception in the first place, however, almost always indicates that you have an underlying data modeling problem that you should really be solving. If you need to query other rows in the table in order to figure out what you want to do with the new rows, that's a pretty good indication that you have improperly normalized your data model and that one row has some dependency on another row in the same table rather than being an autonomous fact. Fixing the data model is almost always preferrable to working around the mutating table exception.

Writing an SQL trigger to find if number appears in column more than X times?

I want to write a Postgres SQL trigger that will basically find if a number appears in a column 5 or more times. If it appears a 5th time, I want to throw an exception. Here is how the table looks:
create table tab(
first integer not null constraint pk_part_id primary key,
second integer constraint fk_super_part_id references bom,
price integer);
insert into tab values(1,NULL,100), (2,1,50), (3,1,30), (4,2,20), (5,2,10), (6,3,20);
Above are the original inserts into the table. My trigger will occur upon inserting more values into the table.
Basically if a number appears in the 'second' column more than 4 times after inserting into the table, I want to raise an exception. Here is my attempt at writing the trigger:
create function check() return trigger as '
begin
if(select first, second, price
from tab
where second in (
select second from tab
group by second
having count(second) > 4)
) then
raise exception ''Error, there are more than 5 parts.'';
end if;
return null;
end
'language plpgsql;
create trigger check
after insert or update on tab
for each row execute procedure check();
Could anyone help me out? If so that would be great! Thanks!
CREATE FUNCTION trg_upbef()
RETURN trigger as
$func$
BEGIN
IF (SELECT count(*)
FROM tab
WHERE second = NEW.second ) > 3 THEN
RAISE EXCEPTION 'Error: there are more than 5 parts.';
END IF;
RETURN NEW; -- must be NEW for BEFORE trigger
END
$func$ LANGUAGE plpgsql;
CREATE TRIGGER upbef
BEFORE INSERT OR UPDATE ON tab
FOR EACH ROW EXECUTE procedure trg_upbef();
Major points
Keyword is RETURNS, not RETURN.
Use the special variable NEW to refer to the newly inserted / updated row.
Use a BEFORE trigger. Better skip early in case of an exception.
Don't count everything for your test, just what you need. Much faster.
Use dollar-quoting. Makes your live easier.
Concurrency:
If you want to be absolutely sure, you'll have to take an exclusive lock on the table before counting. Else, concurrent inserts / updates might outfox each other under heavy concurrent load. While this is rather unlikely, it's possible.