avoid rewriting same code in trigger - sql

I am writing data validations at database level using table.
Following is my table structure used for validations:(with limited records for understanding)
SRNO | COL_NAME | OBJ_NAME | IS_MANDATORY | ALTER_COL | ERROR_MSG
1 | TITLE | Customer | Y | | Enter first name!
2 | FNAME | Customer | Y | LNAME | Either first or last name are required!
3 | MNAME | Customer | N | |
4 | LNAME | Customer | Y | FNAME | Either first or last name are required!
I have written trigger which validates data before inserting it to the database as per validation table:
CREATE OR REPLACE TRIGGER VALIDATE_CUST
BEFORE INSERT ON CUSTOMER REFERENCING NEW AS NEW OLD AS OLD FOR EACH ROW
DECLARE
T_COL_VAL NUMBER(4);
BEGIN
IF INSERTING THEN
FOR C IN
(select COL_NAME,ERROR_MSG from VALIDATE_COLS where IS_MANDATORY='Y' and OBJ_NAME='Customer' order by SRNO)
LOOP
T_COL_VAL := CASE C.COL_NAME
WHEN 'TITLE' THEN nvl(length(:NEW.TITLE),0)
WHEN 'FNAME' THEN nvl(length(:NEW.FNAME),0)
WHEN 'MNAME' THEN nvl(length(:NEW.MNAME),0)
WHEN 'LNAME' THEN nvl(length(:NEW.LNAME),0)
ELSE length('OK')
END;
IF T_COL_VAL=0 THEN
RAISE_APPLICATION_ERROR(-20001,C.ERROR_MSG);
END IF;
END LOOP;
END IF;
END;
Above trigger checks whether all the fields which are mandatory have value or not. Above trigger is working fine.
Now I want to use ALTER_COL column from validations table. ALTER_COL column will be checked in case original column is null. For example either of FNAME or LNAME are mandatory. So if FNAME is not provided then trigger must check for LNAME before raising error, and only if LNAME is also blank then only raise an error.
My question is that will I need to rewrite the case statement in if condition to get the length of ALTER_COL like :
IF T_COL_VAL=0 THEN
--if `ALTER_COL` is not null
--Write case statement again to get the length of `ALTER_COL`
--If length of `ALTER_COL` is 0 then only raise following error
RAISE_APPLICATION_ERROR(-20001,C.ERROR_MSG);
END IF;
Or is there any better way of doing this?
Any suggestion will be appreciated!
Update1(after the answer from #Wernfried)
I tried writing function which can avoid rewriting code but I can't pass NEW to the function. Function which I tried looks like.
CREATE FUNCTION FN_VALIDATE_CUST(COL_NAME in VARCHAR) return NUMBER as
BEGIN
return CASE COL_NAME
WHEN 'FNAME' THEN nvl(length(:NEW.FNAME),0)
WHEN 'MNAME' THEN nvl(length(:NEW.MNAME),0)
WHEN 'LNAME' THEN nvl(length(:NEW.LNAME),0)
WHEN 'TITLE' THEN nvl(length(:NEW.TITLE),0)
ELSE length('OK')
END;
END;
I can't pass the NEW object to the above function to access column value. So I will need to write case statement in trigger event if I use function.

Or is there any better way of doing this?
Definitely -- use NOT NULL and CHECK constraints in the database to do this. That's what they're for, and they're a lot more performant, reliable, and flexible than your approach.
If you really need to provide custom error messages for constraint violations then ensure that the constraints are named, and provide a table of custom messages to be used when the constraint is violated.
Edit ...
If enforcement of the constraints is dependent on the presence of a customer number in another table, then I'd rather have a flag in the table that indicates whether or not to apply the check and not null constraints, and include that in the constraint definitions.
For example:
... add constraint lname_required check (apply_constraints = 0 or lname is not null)
I don't think that in this case you can get away from needing a trigger, as you'd have to maintain the value of "apply_constraints" with one, based on the presence of the customer number in the other table (to ensure that deletions or insertions on that table update the relevant values in this table).

You can write a procedure and call this procedure in your trigger.

Related

Will postgresql allow any sort of concatenation for (double quoted) identifier names?

Most of my constraint names are concise and short, but a few of them are long and including those on a single line exceeds the code style conventions I am working with.
Without thinking about it, I just did a line break and a double pipe, as so:
history uint4 constraint "[tablename] The beginning of history must be sooner " ||
"than the current day" check (history <= hdate)
This of course fails, because it's an identifier and not a string literal. I understand why it fails, and this bums me out because I'm almost certain that there's no way to break and continue the identifier on a second line.
Am I wrong? Is there a trick here? It seems like I should be able to get away with a \ at the end of the line, but then the continuation can't be indented (since those spaces would be part of the identifier name).
You can execute a statement, which contains the original command, formatting, function calls etc. You may want to put the constraint in an alter table statement instead of wrapping the entire table creation statement in the execute
do $$ BEGIN
EXECUTE 'CREATE TABLE tex (' ||
'hdate int, ' ||
'history int constraint "[tablename] The beginning of history must be sooner ' ||
'than the current day" check (history <= hdate));';
end $$;
\d tex
Table "public.tex"
Column | Type | Collation | Nullable | Default
---------+---------+-----------+----------+---------
hdate | integer | | |
history | integer | | |
Check constraints:
"[tablename] The beginning of history must be sooner than the cu" CHECK (history <= hdate)
PS: though the text seems too long and will be truncated, regardless of the string concatenation

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

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.

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.