Trigger Function → Selecting a unique row using a values in a none unique column - sql

I got a sticky situation here whereby I setup a trigger to update a table (Self-updating function). What I got here is that the function is able to identify that there is an update operation however, it cannot located the row to update as there are no unique value in the column.
TRIG_NS_ABS4_To_Area_func (Trigger Func):
BEGIN
IF (TG_OP = 'UPDATE') AND (OLD."ABS4" <> NEW."ABS4")
THEN UPDATE systems."NS_HandoverReportInput_tbl" SET ("Area") = ((SELECT "NS_AREA" FROM systems."NS_ABS4Area Match_tbl" WHERE "NS_ABS4" = NEW."ABS4"))
WHERE "NSItemNumber" = NEW."NSItemNumber";
END IF;
RETURN NEW;
END;
I was wondering whether does anyone have any idea to locate the row to update.
Please bear in mind that ONLY the "NSItemNumber" field is unique else the rest of the fields may have repeating values.
Script:
CREATE TABLE systems."NS_HandoverReportInput_tbl" (
"NSItemNumber" SERIAL,
"ABS4" TEXT,
"Area" TEXT,
CONSTRAINT "PK_NS_HandoverReportInput_tbl" PRIMARY KEY("NSItemNumber"),
)
WITH (oids = false);
CREATE TRIGGER "NS_ABS4Area Match_tbl"
AFTER INSERT OR UPDATE
ON systems."NS_HandoverReportInput_tbl" FOR EACH ROW
EXECUTE PROCEDURE systems."TRIG_NS_ABS4_To_Area_func"();
NS_ABS4Area Match_tbl display info as listed:
NSItemNumber | ABS4 | Area
1001 | AAAA |Toilet
1002 | AABB |Central Area
1003 | AACC |Carpark
1004 | AAAA |Toilet
1005 | AABB |Central Area

I'll give you two solutions, a working one and a good one.
The working solution
Use a BEFORE INSERT OR UPDATE trigger, then you don't have to locate the row to update, because instead of changing the table you change the values before they are written to the table.
You could define your trigger like this:
CREATE OR REPLACE FUNCTION "TRIG_NS_ABS4_To_Area_func"() RETURNS trigger LANGUAGE plpgsql AS
$$BEGIN
-- this will fail if there is more than one "NS_AREA" per "NS_ABS4"
SELECT DISTINCT "NS_AREA" INTO STRICT NEW."NS_AREA"
FROM "NS_ABS4Area Match_tbl"
WHERE "NS_ABS4" = NEW."ABS4";
RETURN NEW;
END;$$;
CREATE TRIGGER "NS_ABS4Area Match_tbl"
BEFORE INSERT OR UPDATE
ON systems."NS_HandoverReportInput_tbl" FOR EACH ROW
EXECUTE PROCEDURE systems."TRIG_NS_ABS4_To_Area_func"();
The good solution
You avoid the whole mess by normalizing your database design.
That way no inconsistencies can ever happen, and you don't need a trigger.
CREATE TABLE area_description (
abs4 text PRIMARY KEY,
area text NOT NULL
);
COPY area_description FROM STDIN (FORMAT 'csv');
'AAAA', 'Toilet'
'AABB', 'Central Area'
'AACC', 'Carpark'
\.
CREATE TABLE ns_report_input (
ns_item_number serial PRIMARY KEY,
abs4 text REFERENCES area_description(abs4)
);
CREATE INDEX ns_report_input_fkex_ind ON ns_report_input(abs4);
You can define a view if you want something that looks like your original table.

Related

How can I make a unique constraint order independently on two columns

I'm working with an Oracle Database and I need to create a table like below.
MAP(Point_One, Poin_Two, Connection_weight).
The table represents data about a graph. I would like to create a table with a constraint that prevents the insertion of an already existing connection.
For example, the table already contains this connection:
Point_One | Point_Two | Connection_weight
-----------------------------------------
p_no1 | p_no2 | 10
And the constraint would prevent the repeated insertion of this connection, even if I try to add the points in different order. (For example: (p_no2, p_no1, 10) )
A simple UNIQUE (Point_One, Point_Two) constraint is unfortunatelly not enough. Do you have any advice?
You can create a function-based index
CREATE UNIQUE INDEX idx_unique_edge
ON map( greatest( point_one, point_two ),
least( point_one, point_two ) );
I'm assuming that the data type of point_one and point_two is compatible with the Oracle greatest and least functions. If not, you'd need a function of your own that picks the "greatest" and "least" point for your complex data type.
You can achieve the desired result easily by using trigger.
create table map (point_one number, point_two number, connection_weight number)
/
create or replace trigger tr_map before insert on map
for each row
declare
c number;
begin
select count(1) into c from map where (point_one=:new.point_one and point_two=:new.point_two)
or
(point_one=:new.point_two and point_two=:new.point_one);
if c>0 then
raise_application_error(-20000,'Connection line already exists');
end if;
end;
/
SQL> insert into map values (1,2,10);
1 row created.
SQL> insert into map values (2,1,10);
insert into map values (2,1,10)
*
ERROR at line 1:
ORA-21000: error number argument to raise_application_error of -100 is out of
range
ORA-06512: at "C##MINA.TR_MAP", line 10
ORA-04088: error during execution of trigger 'C##MINA.TR_MAP'
I'm still thinking about the CHECK constraint, but yet I didn't come with decission whether it is possible or not.

Writing an SQL trigger to compare old and new values

I am trying to write a SQL trigger that compares the old and new values. If the two values are different then I need to display an error saying that you can't update the names. The exact definition of my trigger is
write a trigger function named disallow_team_name_update that compares
the OLD and NEW records team fields. If they are different raise an
exception that states that changing the team name is not allowed.
Then, attach this trigger to the table with the name tr disallow team
name update and specify that it fires before any potential update of
the team field in the table.
The table that I am using for this problem is:
Table "table.group_standings"
Column | Type | Modifiers
--------+-----------------------+-----------
team | character varying(25) | not null
wins | smallint | not null
losses | smallint | not null
draws | smallint | not null
points | smallint| not null
Indexes:
"group_standings_pkey" PRIMARY KEY, btree (team)
Check constraints:
"group_standings_draws_check" CHECK (draws >= 0)
"group_standings_losses_check" CHECK (losses >= 0)
"group_standings_points_check" CHECK (points >= 0)
"group_standings_wins_check" CHECK (wins >= 0)
Here is my code:
CREATE OR REPLACE FUNCTION disallow_team_name_update() RETURNS trigger AS $$
BEGIN
if(NEW.team <> OLD.team)
/*tell the user to not change team names*/
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER tr_disallow_team_name_update BEFORE INSERT OR UPDATE OF team ON group_standings
FOR EACH ROW EXECUTE PROCEDURE disallow_team_name_update();
PostgreSQL can use raise exception to, um, raise exceptions.
CREATE OR REPLACE FUNCTION disallow_team_name_update()
RETURNS trigger AS
$$
BEGIN
if(NEW.team <> OLD.team) then
raise exception 'Invalid update.'
using hint = 'Changes to team name are not allowed.';
end if;
END
$$
LANGUAGE plpgsql;
You surely don't want to disallow changes to the team name on insert. (PostgreSQL won't let you do it anyway.)
CREATE TRIGGER tr_disallow_team_name_update
BEFORE UPDATE OF team ON group_standings
FOR EACH ROW EXECUTE PROCEDURE disallow_team_name_update();

How do you ensure that a table is only updated when a new modified_at is greater than the current modified_at in PostgreSQL?

I have a situation where I want to deny an update to a table if the current modified_at column is more recent than the one specified in the update. I attempted to do this with a CHECK constraint, but it has no effect.
Here is an example:
CREATE TABLE test (
id SERIAL PRIMARY KEY,
value INTEGER NOT NULL DEFAULT 0,
modified_at timestamptz DEFAULT NOW()
);
ALTER TABLE test ADD CHECK (modified_at >= modified_at);
INSERT INTO test (id, value) VALUES (1, 1);
INSERT 0 1
SELECT * FROM test;
id | value | modified_at
----+-------+-------------------------------
1 | 1 | 2013-05-30 14:34:37.234456-07
UPDATE test
SET value = 2, modified_at = NOW() - INTERVAL '1 day'
WHERE id = 1;
UPDATE 1
SELECT * FROM test;
id | value | modified_at
----+-------+-------------------------------
1 | 2 | 2013-05-29 14:35:41.337543-07
This doesn't appear to work as expected. Intuitively I could see this being a problem. How does the planner know that the left hand side should be the new value and the right hand side should be the old?
Knowing this doesn't work, how should this constraint be enforced?
You will have to check old modified_date against new modified_date of the updated row, and you can do this using triggers.
Set the trigger to run on each row before update and create a function that deals with it, choosing if you want to keep the old modified_date or if you don't want to perform any update at all.
The trigger can be done like this:
CREATE TRIGGER trigger_test
BEFORE INSERT OR UPDATE
ON test
FOR EACH ROW
EXECUTE PROCEDURE fn_trigger_test();
And the function like this:
CREATE OR REPLACE FUNCTION fn_trigger_test()
RETURNS trigger AS
$BODY$
DECLARE
BEGIN
IF (TG_OP = 'UPDATE') THEN
IF NEW.modified_at<OLD.modified_at THEN
RAISE EXCEPTION 'Date_modified older than previous';
/* or to keep the old modified date:
NEW.modifed_at=OLD.modified_at;
RETURN NEW; */
ELSE
RETURN NEW;
END IF;
END IF;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
As far as CHECK constraints are concerned, there's no such thing as new values and old values.
PostgreSQL triggers have access to new data. Another fairly common approach is to revoke permissions on the tables, and require all access to take place through stored procedures. Stored procedures can have access to new data, passed through parameters, and stored procs can check values in tables, update additional tables for auditing, etc. See CREATE FUNCTION and plpgsql.

UPDATE OR INSERT function when uniqueness is determined by the combination of two columns

Expanding on the usual INSERT OR UPDATE conondrum
I have a function that serves normal UPDATE OR INSERT, and it looks like this:
CREATE OR REPLACE FUNCTION updateslave ( varchar(7), smallint ) RETURNS void AS
$$
BEGIN
LOOP
UPDATE consist SET
master = $1,
time = now() WHERE slave = $2;
IF found THEN
RETURN;
END IF;
BEGIN
INSERT INTO consist(master, slave, time) VALUES ( $1, $2, now() );
RETURN;
EXCEPTION WHEN unique_violation THEN
-- do nothing, then loop and retry
END;
END LOOP;
END;
##
LANGUAGE plpgsql;
Now, the issue is that i'm trying to rewrite it for a similar operation in a different, table. However, the difference is that in this other table there is no single unique column, but the combination of two columns only exists in one row. Is it possible to declare unique_violation based on the combination of two columns instead?
For the sake of keeping examples simple, let's assume that the table looks exactly the same as the one i use the above function for, but with master and slave being the two columns that together produce uniqueness:
Column | Type | Modifiers | Storage | Description
--------+-----------------------------+----------------------------------------------+----------+-------------
master | character varying(7) | not null default 'unused'::character varying | extended |
slave | smallint | not null | plain |
time | timestamp without time zone | default now() | plain |
The best approach is to define a unique constraint on the table, that way no matter how the update happens, ie if your proc is used or not, everything is OK.
The easiest way to do that is to create a unique index over the two columns:
create unique index any_mame on mytable(col1, col2);
You can also alter the table to add a unique constraint, but there's not much difference.

PLPGSQL Cascading Triggers?

I am trying to create a trigger, so that when ever I add a new record it adds another record in the same table. The session field will only take values between 1 and 4. So when I add a 1 in session I want it to add another record but with session 3 blocked. But the problem is that it leads to cascading triggers and it inserts itself again and again because the trigger is triggered when inserted.
I have for example a simple table:
CREATE TABLE example
(
id SERIAL PRIMARY KEY
,name VARCHAR(100) NOT NULL
,session INTEGER
,status VARCHAR(100)
);
My trigger function is:
CREATE OR REPLACE FUNCTION add_block() RETURNS TRIGGER AS $$
BEGIN
INSERT INTO example VALUES (NEW.id + 1, NEW.name, NEW.session+2, 'blocked');
RETURN NULL;
END;
$$ LANGUAGE 'plpgsql';
Trigger is:
CREATE TRIGGER add_block
AFTER INSERT OR UPDATE
ON example
FOR EACH ROW
EXECUTE PROCEDURE add_block();
I get error:
SQL statement "INSERT INTO example VALUES ( $1 +1, $2 , $3 + 2, $4)"
PL/pgSQL function "add_block" line 37 at SQL statement
This error repeats itself so many times that I can't see the top.
How would I solve this?
EDIT:
CREATE TABLE block_rules
(
id SERIAL PRIMARY KEY
,session INTEGER
,block_session INTEGER
);
This table holds the block rules. So if a new record is inserted into the EXAMPLE table with session 1 then it blocks session 3 accordingly by inserting a new record with blocked status in the same (EXAMPLE) table above (not block_rules). Same for session 2 but it blocks session 4.
The block_rules table holds the rules (or pattern) to block a session by. It holds
id | session | block_session
------------------------------
1 | 1 | 3
2 | 2 | 4
3 | 3 | 2
How would I put that in the WHEN statement of the trigger going with Erwin Branstetter's answer below?
Thanks
New answer to edited question
This trigger function adds blocked sessions according to the information in table block_rules.
I assume that the tables are linked by id - information is missing in the question.
I now assume that the block rules are general rules for all sessions alike and link by session. The trigger is only called for non-blocked sessions and inserts a matching blocked session.
Trigger function:
CREATE OR REPLACE FUNCTION add_block()
RETURNS TRIGGER AS
$BODY$
BEGIN
INSERT INTO example (name, session, status)
VALUES (NEW.name
,(SELECT block_session
FROM block_rules
WHERE session = NEW.session)
,'blocked');
RETURN NULL;
END;
$BODY$ LANGUAGE plpgsql;
Trigger:
CREATE TRIGGER add_block
AFTER INSERT -- OR UPDATE
ON example
FOR EACH ROW
WHEN (NEW.status IS DISTINCT FROM 'blocked')
EXECUTE PROCEDURE add_block();
Answer to original question
There is still room for improvement. Consider this setup:
CREATE OR REPLACE FUNCTION add_block()
RETURNS TRIGGER AS
$BODY$
BEGIN
INSERT INTO example (name, session, status)
VALUES (NEW.name, NEW.session + 2, 'blocked');
RETURN NULL;
END;
$BODY$ LANGUAGE plpgsql;
CREATE TRIGGER add_block
AFTER INSERT -- OR UPDATE
ON example
FOR EACH ROW
WHEN (NEW.session < 3)
-- WHEN (status IS DISTINCT FROM 'blocked') -- alternative guess at filter
EXECUTE PROCEDURE add_block();
Major points:
For PostgreSQL 9.0 or later you can use a WHEN condition in the trigger definition. This would be most efficient. For older versions you use the same condition inside the trigger function.
There is no need to add a column, if you can define criteria to discern auto-inserted rows. You did not tell, so I assume that only auto-inserted rows have session > 2 in my example. I added an alternative WHEN condition for status = 'blocked' as comment.
You should always provide a column list for INSERTs. If you don't, later changes to the table may have unexpected side effects!
Do not insert NEW.id + 1 in the trigger manually. This won't increment the sequence and the next INSERT will fail with a duplicate key violation.
id is a serial column, so don't do anything. The default nextval() from the sequence is inserted automatically.
Your description only mentions INSERT, yet you have a trigger AFTER INSERT OR UPDATE. I cut out the UPDATE part.
The keyword plpgsql doesn't have to be quoted.
OK so can't you just add another column, something like this:
ALTER TABLE example ADD COLUMN trig INTEGER DEFAULT 0;
CREATE OR REPLACE FUNCTION add_block() RETURNS TRIGGER AS $$
BEGIN
IF NEW.trig = 0 THEN
INSERT INTO example VALUES (NEXTVAL('example_id_seq'::regclass), NEW.name, NEW.session+2, 'blocked', 1);
END IF;
RETURN NULL;
END;
$$ LANGUAGE 'plpgsql';
it's not great, but it works :-)
CREATE OR REPLACE FUNCTION add_block() RETURNS TRIGGER AS $$
BEGIN
SET SESSION session_replication_role = replica;
INSERT INTO example VALUES (NEXTVAL('example_id_seq'::regclass), NEW.name, NEW.session+2, 'blocked');
SET SESSION session_replication_role = origin;
RETURN NULL;
END;
$$ LANGUAGE 'plpgsql';