DB2 how to prevent to delete values (with 'before delete' trigger)? - db2

I have code like this:
CREATE TABLE A10.STUDENTS
( IDSTUDENT INTEGER NOT NULL,
LASTNAME VARCHAR(30) NOT NULL,
FIRSTNAME VARCHAR(30) NOT NULL,
MARK DECIMAL(5,1) NOT NULL,
PRIMARY KEY ( IDSTUDENT )
);
ALTER TABLE A10.STUDENTS ADD CONSTRAINT CHECK_MARK CHECK(CAST(MARK AS DECIMAL(5,1)) IN (2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0));
INSERT INTO A10.STUDENTS (LASTNAME,FIRSTNAME,MARK) VALUES ('Smith','John',2.0);
and I want to add "before delete" trigger, that prevent deleting marks under 3.0.
-- <ScriptOptions statementTerminator="#" />
CREATE TRIGGER A10.DONT_DELETE_MARKS
NO CASCADE BEFORE DELETE
ON A10.STUDENTS
REFERENCING NEW AS N OLD AS O
FOR EACH ROW MODE DB2SQL NOT SECURED
BEGIN
--and what here?
END#
-- <ScriptOptions statementTerminator=";" />
I have tried something like:
raise_application_error(-20001,'Records can not be deleted');
but it doesn't work.
Thanks for help :)

What does "doesn't work" mean?
What platform and version of Db2?
Try
signal sqlstate xxxxx
set message_text = 'Records can not be deleted';
replace xxxxx with the SQL Statecode you wish to use. Note that
SQLSTATE values are comprised of a two-character class code value,
followed by a three-character subclass code value. Class code values
represent classes of successful and unsuccessful completion
conditions. If you want to use SQLSTATE as the basis of your
application's return codes, you can define your own SQLSTATE classes
or subclasses using the following guidelines:
SQLSTATE classes that begin with the characters 7 through 9 or I through Z can be defined. Within these classes, any subclass can be
defined.
SQLSTATE classes that begin with the characters 0 through 6 or A through H are reserved for the database manager. Within these classes,
subclasses that begin with the characters 0 through H are reserved for
the database manager. Subclasses that begin with the characters I
through Z can be defined.

Related

Error key already exists appears only while executing function

I have 2 tables, in one I create an element, in the other I save the details of said element in different languages.
The first table:
CREATE TABLE public.element
(
idelement integer NOT NULL DEFAULT nextval('element_idelement_seq'::regclass),
image_location text,
price numeric(6,2) NOT NULL DEFAULT 0,
CONSTRAINT element_pkey PRIMARY KEY (idelement)
)
the second table:
CREATE TABLE public.elementdetails
(
idelement integer NOT NULL,
title text,
description text,
notes text,
language text NOT NULL,
CONSTRAINT elementdetails_pkey PRIMARY KEY (idelement, language)
)
I use a function to insert the new elements in plpgsql
IF _idelement = 0 THEN
INSERT INTO element (price) VALUES (0.0);
SELECT lastval() INTO _idelement;
EXECUTE FORMAT('INSERT INTO elementdetails(idelement, %I, language) VALUES (%L, %L, %L))', _fieldname, _idelement, _value, _language);
END IF;
But it raises an error saying the key constraints (idelement, language) already exists.
Simply copying the SQL appearing in the error message and executing it in another window would work with no issues and the problem appears only while within the pgsql function.
EDIT: I'll add that this worked until I noticed an issue where I let people insert the language iso both in uppercase or lowercase, so I forced the language to always be uppercase, since then I get the error message.
I found the problem:
right after that statement I had another one
IF _idelement > 0 THEN
EXECUTE FORMAT('INSERT INTO elementdetails(idelement, %I, language) VALUES (%L, %L, %L))', _fieldname, _idelement, _value, _language);
END IF;
but I forgot to put it after an ELSE so both INSERTs were executed which raised the error.

Return the value changed by an update without a trigger

Postgres has a great RETURNING clause for INSERT, DELETE and UPDATE...and it's made me a bit greedy. In a few cases, what I'd like to get is not only the current value, but the previous value:
UPDATE analytic_productivity
SET points = 1000
WHERE points > 1000
RETURNING id,
points,
OLD.points;
I don't believe there's any way to access previous values outside of the lifespan and context of a trigger. So, I'll guess what I'd like isn't possible as such. If that's right, can anyone suggest an alternative? I'm overwriting outliers with some set values, and would like to record the modified values in another table. This is why I don't know the current value in advance. This is a rare (and clearly suspect) operation, and I don't want to record the change on normal inserts and updates.
As an alternative, I'm thinking that I can select the outliers, revise them, and then write back the modifications. So, do most of the work on the client side with a couple of requests to Postgres. If so, can someone suggest the right locking level to apply between my initial SELECT and my following UPDATE? I believe that the FOR UPDATE lock is right.
Any suggestions on a smart way to capture previous values, during an update, without a trigger would be great to hear about.
Follow-up
Thanks to comments here, I experimented a bit and came up with a solution that works in my case. To make my objectives clearer:
I've got a table named outlier_rule that defines values that are too high for a specific column.
The goal is to loop over the table, and apply the rules to set outliers to a fixed value.
Stomping on outliers like this is...questionable. There must be leaks in the app's UI that allow for unreasonable values. To help track these down, I'm recording the large values in a table named outlier_change.
I'd like to push this behavior into server-side function so that any of our servers, regardless of their codebase version, can invoke the current logic.
The client servers compose and send an email with a result summary, when outliers are found and corrected.
So, a server-side function to do everything, log some data, and return a result. I've got that working, but it's got the smell of You Don't Know What You're Doing So Just Keep Adding Code Until it Works. I've at least got a better handle on using FORMAT and think I understand now that a single function can do many things, and that you can choose what to return with the RETURN clause. For reference, the various bits of code:
CREATE TABLE IF NOT EXISTS data.outlier_rule (
id uuid NOT NULL DEFAULT extensions.gen_random_uuid(),
schema_name text NOT NULL DEFAULT NULL,
table_name text NOT NULL DEFAULT NULL,
column_name text NOT NULL DEFAULT NULL,
threshold integer,
set_to integer,
CONSTRAINT outlier_rule_id_pkey
PRIMARY KEY (schema_name,table_name,column_name)
);
For tracking the modifications, I've got a second table named outlier_change:
------------------------------
-- Table
------------------------------
DROP TABLE IF EXISTS data.outlier_change CASCADE;
CREATE TABLE IF NOT EXISTS data.outlier_change (
id uuid NOT NULL DEFAULT NULL,
outlier_rule_id uuid NOT NULL DEFAULT NULL,
value_was integer NOT NULL DEFAULT NULL,
set_to integer NOT NULL DEFAULT NULL,
change_count integer NOT NULL DEFAULT 0,
last_changed_dts timestamptz NOT NULL DEFAULT NOW(),
CONSTRAINT outlier_change_id_pkey
PRIMARY KEY (id,outlier_rule_id)
);
ALTER TABLE data.outlier_change OWNER TO user_change_structure;
------------------------------
-- Trigger Function
------------------------------
CREATE OR REPLACE FUNCTION data.on_outlier_change_upsert()
RETURNS pg_catalog.trigger AS $BODY$
BEGIN
NEW.last_changed_dts := NOW();
NEW.change_count := OLD.change_count + 1;
RETURN NEW; -- important!
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
------------------------------
-- Trigger
------------------------------
CREATE TRIGGER outlier_change_upsert BEFORE INSERT OR UPDATE ON data.outlier_change
FOR EACH ROW
EXECUTE PROCEDURE data.on_outlier_change_upsert();
DROP FUNCTION IF EXISTS data.outlier_fix ();
CREATE OR REPLACE FUNCTION data.outlier_fix ()
RETURNS TABLE (
schema_name text,
table_name text,
column_name text,
id uuid,
value_was integer,
set_to integer,
change_count integer
)
AS $$
DECLARE
rule record;
now_ timestamptz = NOW();
BEGIN
FOR rule IN SELECT * FROM data.outlier_rule LOOP
EXECUTE FORMAT (
'INSERT INTO outlier_change (
outlier_rule_id,
set_to,
id,
value_was)
SELECT %6$L,
%5$s,
%2$I.id,
%2$I.%3$I
FROM %1$I.%2$I
WHERE %3$I > %4$s
ON CONFLICT(id,outlier_rule_id) DO UPDATE SET
value_was = EXCLUDED.value_was,
set_to = EXCLUDED.set_to
RETURNING outlier_rule_id,
id,
value_was,
set_to
change_count;
UPDATE %1$I.%2$I
SET %3$I = %5$s
WHERE %3$I > %4$s;',
rule.schema_name,
rule.table_name,
rule.column_name,
rule.threshold,
rule.set_to,
rule.id);
END LOOP;
RETURN QUERY EXECUTE ('
SELECT outlier_rule.schema_name,
outlier_rule.table_name,
outlier_rule.column_name,
outlier_change.id,
outlier_change.value_was,
outlier_change.set_to,
outlier_change.change_count
FROM outlier_change
JOIN outlier_rule ON (outlier_rule.id = outlier_change.outlier_rule_id)
WHERE last_changed_dts = $1')
USING now_;
END;
$$ LANGUAGE plpgsql;
ALTER FUNCTION data.outlier_fix() OWNER TO user_bender;
You could achieve that with a bit of a hack. You can self join the table in your update query like this:
UPDATE analytic_productivity NEW
SET points = 1000
FROM analytic_productivity OLD
WHERE NEW.points > 1000
and NEW.id = OLD.id
RETURNING NEW.id,
NEW.points,
OLD.points as old_points;

How to use variable settings in trigger functions?

I would like to record the id of a user in the session/transaction, using SET, so I could be able to access it later in a trigger function, using current_setting. Basically, I'm trying option n2 from a very similar ticket posted previously, with the difference that I'm using PG 10.1 .
I've been trying 3 approaches to setting the variable:
SET local myvars.user_id = 4, thereby setting it locally in the transaction;
SET myvars.user_id = 4, thereby setting it in the session;
SELECT set_config('myvars.user_id', '4', false), which depending of the last argument, will be a shortcut for the previous 2 options.
None of them is usable in the trigger, which receives NULL when getting the variable through current_setting. Here is a script I've devised to troubleshoot it (can be easily used with the postgres docker image):
database=$POSTGRES_DB
user=$POSTGRES_USER
[ -z "$user" ] && user="postgres"
psql -v ON_ERROR_STOP=1 --username "$user" $database <<-EOSQL
DROP TRIGGER IF EXISTS add_transition1 ON houses;
CREATE TABLE IF NOT EXISTS houses (
id SERIAL NOT NULL,
name VARCHAR(80),
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now(),
PRIMARY KEY(id)
);
CREATE TABLE IF NOT EXISTS transitions1 (
id SERIAL NOT NULL,
house_id INTEGER,
user_id INTEGER,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now(),
PRIMARY KEY(id),
FOREIGN KEY(house_id) REFERENCES houses (id) ON DELETE CASCADE
);
CREATE OR REPLACE FUNCTION add_transition1() RETURNS TRIGGER AS \$\$
DECLARE
user_id integer;
BEGIN
user_id := current_setting('myvars.user_id')::integer || NULL;
INSERT INTO transitions1 (user_id, house_id) VALUES (user_id, NEW.id);
RETURN NULL;
END;
\$\$ LANGUAGE plpgsql;
CREATE TRIGGER add_transition1 AFTER INSERT OR UPDATE ON houses FOR EACH ROW EXECUTE PROCEDURE add_transition1();
BEGIN;
%1% SELECT current_setting('myvars.user_id');
%2% SELECT set_config('myvars.user_id', '55', false);
%3% SELECT current_setting('myvars.user_id');
INSERT INTO houses (name) VALUES ('HOUSE PARTY') RETURNING houses.id;
SELECT * from houses;
SELECT * from transitions1;
COMMIT;
DROP TRIGGER IF EXISTS add_transition1 ON houses;
DROP FUNCTION IF EXISTS add_transition1;
DROP TABLE transitions1;
DROP TABLE houses;
EOSQL
The conclusion I came to was that the function is triggered in a different transaction and a different (?) session. Is this something that one can configure, so that all happens within the same context?
Handle all possible cases for the customized option properly:
option not set yet
All references to it raise an exception, including current_setting() unless called with the second parameter missing_ok. The manual:
If there is no setting named setting_name, current_setting throws an error unless missing_ok is supplied and is true.
option set to a valid integer literal
option set to an invalid integer literal
option reset (which burns down to a special case of 3.)
For instance, if you set a customized option with SET LOCAL or set_config('myvars.user_id3', '55', true), the option value is reset at the end of the transaction. It still exists, can be referenced, but it returns an empty string now ('') - which cannot be cast to integer.
Obvious mistakes in your demo aside, you need to prepare for all 4 cases. So:
CREATE OR REPLACE FUNCTION add_transition1()
RETURNS trigger AS
$func$
DECLARE
_user_id text := current_setting('myvars.user_id', true); -- see 1.
BEGIN
IF _user_id ~ '^\d+$' THEN -- one or more digits?
INSERT INTO transitions1 (user_id, house_id)
VALUES (_user_id::int, NEW.id); -- valid int, cast is safe
ELSE
INSERT INTO transitions1 (user_id, house_id)
VALUES (NULL, NEW.id); -- use NULL instead
RAISE WARNING 'Invalid user_id % for house_id % was reset to NULL!'
, quote_literal(_user_id), NEW.id; -- optional
END IF;
RETURN NULL; -- OK for AFTER trigger
END
$func$ LANGUAGE plpgsql;
db<>fiddle here
Notes:
Avoid variable names that match column names. Very error prone. One popular naming convention is to prepend variable names with an underscore: _user_id.
Assign at declaration time to save one assignment. Note the data type text. We'll cast later, after sorting out invalid input.
Avoid raising / trapping an exception if possible. The manual:
A block containing an EXCEPTION clause is significantly more expensive
to enter and exit than a block without one. Therefore, don't use
EXCEPTION without need.
Test for valid integer strings. This simple regular expression allows only digits (no leading sign, no white space): _user_id ~ '^\d+$'. I reset to NULL for any invalid input. Adapt to your needs.
I added an optional WARNING for your debugging convenience.
Cases 3. and 4. only arise because customized options are string literals (type text), valid data types cannot be enforced automatically.
Related:
User defined variables in PostgreSQL
Is there a way to define a named constant in a PostgreSQL query?
All that aside, there may be more elegant solutions for what you are trying to do without customized options, depending on your exact requirements. Maybe this:
Fastest way to get current user's OID in Postgres?
It is not clear why you are trying to concat NULL to user_id but it is obviously the cause of the problem. Get rid of it:
CREATE OR REPLACE FUNCTION add_transition1() RETURNS TRIGGER AS $$
DECLARE
user_id integer;
BEGIN
user_id := current_setting('myvars.user_id')::integer;
INSERT INTO transitions1 (user_id, house_id) VALUES (user_id, NEW.id);
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
Note that
SELECT 55 || NULL
always gives NULL.
You can catch the exception when the value doesn't exist - here's the changes I made to get this to work:
CREATE OR REPLACE FUNCTION add_transition1() RETURNS TRIGGER AS $$
DECLARE
user_id integer;
BEGIN
BEGIN
user_id := current_setting('myvars.user_id')::integer;
EXCEPTION WHEN OTHERS THEN
user_id := 0;
END;
INSERT INTO transitions1 (user_id, house_id) VALUES (user_id, NEW.id);
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION insert_house() RETURNS void as $$
DECLARE
user_id integer;
BEGIN
PERFORM set_config('myvars.user_id', '55', false);
INSERT INTO houses (name) VALUES ('HOUSE PARTY');
END; $$ LANGUAGE plpgsql;

how to create a column constant in Postgresql

create table test(
t_id SERIAL primary key,
t_date CONSTANT date default CURRENT_DATE
);
ERROR: syntax error at or near "date"
LINE 3: t_date CONSTANT date default CURRENT_DATE
^
********** Error **********
ERROR: syntax error at or near "date"
SQL state: 42601
For a default value you can use a function,
CREATE TABLE test(
t_id SERIAL primary key,
t_date date DEFAULT now()
);
about constant, I never used, even other SQL (!), only in a PL/SQL context ...
If you need a "no update" constraint, you can use a trigger. Example:
CREATE FUNCTION correct_update() RETURNS trigger AS $$
BEGIN
NEW.t_date=OLD.t_date;
RETURN NEW;
END
$$ LANGUAGE plpgsql;
CREATE TRIGGER no_date_update
BEFORE BEFORE UPDATE ON test
FOR EACH ROW
WHEN (OLD.t_date IS DISTINCT FROM NEW.t_date)
EXECUTE PROCEDURE correct_update();
For a complete control, you need also trigg the INSERT event, (and does not need a default value anymore because insert trigger will do):
create table test(
t_id SERIAL primary key,
t_date date -- a default will be redundant
);
CREATE FUNCTION correct_date() RETURNS trigger AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
NEW.t_date=now(); -- default value
ELSIF TG_OP = 'UPDATE' THEN -- optional AND OLD.t_date != NEW.t_date
NEW.t_date=OLD.t_date; -- "constant" behaviour
END IF;
RETURN NEW;
END
$$ LANGUAGE plpgsql;
CREATE TRIGGER constant_date
BEFORE INSERT OR UPDATE ON test
FOR EACH ROW
EXECUTE PROCEDURE correct_date();
The OLD.t_date != NEW.t_date comparison is optional, because not affects performance... But is a good practice to use it. Another way is to check in the trigger, by WHEN, but only update triggers can use OLD... So, the best create-triggers for the same correct_date() function (with no old/new comparison) are:
CREATE TRIGGER constant_date_ins
BEFORE INSERT ON test
FOR EACH ROW
EXECUTE PROCEDURE correct_date();
CREATE TRIGGER constant_date_upd
BEFORE UPDATE ON test
FOR EACH ROW
WHEN (OLD.t_date IS DISTINCT FROM NEW.t_date)
EXECUTE PROCEDURE correct_date();
Contextualizing in a scenario
As commented above in the question, there are a lack of contextualization , ex. explaining "why you think this should work and what it should do".
Scenario-1: the db-master need to block careless programmers
We can imagine a framework like CakePHP with a "created" field and a database-master that wants that this field have a "constant behaviour", preventing that careless programmers affects this "expected constraint".
That kind of scenario was used in the anwser.
Scenario-2: the project decision is to alert by error
This is the suggestion #IgorRomanchenko ...
... now here as a Wiki, you can EDIT and add new solution/example ...
You want a check constraint
create table test(
t_id SERIAL primary key,
t_date date default CURRENT_DATE check(t_date = current_date)
);
insert into test(t_date) values (default);
INSERT 0 1
insert into test(t_date) values ('2014-01-01');
ERROR: new row for relation "test" violates check constraint "test_t_date_check"
DETAIL: Failing row contains (2, 2014-01-01).
Or may be a foreign key constraint which allows multiple possible values and can be updated without altering the table's schema
create table date_constraint (
date_constraint date primary key
);
insert into date_constraint (date_constraint) values (current_date);
create table test(
t_id SERIAL primary key,
t_date date
default CURRENT_DATE
references date_constraint(date_constraint)
);
insert into test(t_date) values (default);
INSERT 0 1
insert into test(t_date) values ('2014-01-01');
ERROR: insert or update on table "test" violates foreign key constraint "test_t_date_fkey"
DETAIL: Key (t_date)=(2014-01-01) is not present in table "date_constraint".
http://www.postgresql.org/docs/current/static/ddl-constraints.html

postgresql: nested insert

I have two tables. Lets say tblA and tblB.
I need to insert a row in tblA and use the returned id as a value to be inserted as one of the columns in tblB.
I tried finding out this in documentation but could not get it. Well, is it possible to write a statement (intended to be used in prepared) like
INSERT INTO tblB VALUES
(DEFAULT, (INSERT INTO tblA (DEFAULT, 'x') RETURNING id), 'y')
like we do for SELECT?
Or should I do this by creating a Stored Procedure?. I'm not sure if I can create a prepared statement out of a Stored Procedure.
Please advise.
Regards,
Mayank
You'll need to wait for PostgreSQL 9.1 for this:
with
ids as (
insert ...
returning id
)
insert ...
from ids;
In the meanwhile, you need to use plpgsql, a temporary table, or some extra logic in your app...
This is possible with 9.0 and the new DO for anonymous blocks:
do $$
declare
new_id integer;
begin
insert into foo1 (id) values (default) returning id into new_id;
insert into foo2 (id) values (new_id);
end$$;
This can be executed as a single statement. I haven't tried creating a PreparedStatement out of that though.
Edit
Another approach would be to simply do it in two steps, first run the insert into tableA using the returning clause, get the generated value through JDBC, then fire the second insert, something like this:
PreparedStatement stmt_1 = con.prepareStatement("INSERT INTO tblA VALUES (DEFAULT, ?) returning id");
stmt_1.setString(1, "x");
stmt_1.execute(); // important! Do not use executeUpdate()!
ResultSet rs = stmt_1.getResult();
long newId = -1;
if (rs.next()) {
newId = rs.getLong(1);
}
PreparedStatement stmt_2 = con.prepareStatement("INSERT INTO tblB VALUES (default,?,?)");
stmt_2.setLong(1, newId);
stmt_2.setString(2, "y");
stmt_2.executeUpdate();
You can do this in two inserts, using currval() to retrieve the foreign key (provided that key is serial):
create temporary table tb1a (id serial primary key, t text);
create temporary table tb1b (id serial primary key,
tb1a_id int references tb1a(id),
t text);
begin;
insert into tb1a values (DEFAULT, 'x');
insert into tb1b values (DEFAULT, currval('tb1a_id_seq'), 'y');
commit;
The result:
select * from tb1a;
id | t
----+---
3 | x
(1 row)
select * from tb1b;
id | tb1a_id | t
----+---------+---
2 | 3 | y
(1 row)
Using currval in this way is safe whether in or outside of a transaction. From the Postgresql 8.4 documentation:
currval
Return the value most recently
obtained by nextval for this sequence
in the current session. (An error is
reported if nextval has never been
called for this sequence in this
session.) Because this is returning a
session-local value, it gives a
predictable answer whether or not
other sessions have executed nextval
since the current session did.
You may want to use AFTER INSERT trigger for that. Something along the lines of:
create function dostuff() returns trigger as $$
begin
insert into table_b(field_1, field_2) values ('foo', NEW.id);
return new; --values returned by after triggers are ignored, anyway
end;
$$ language 'plpgsql';
create trigger trdostuff after insert on table_name for each row execute procedure dostuff();
after insert is needed because you need to have the id to reference it. Hope this helps.
Edit
A trigger will be called in the same "block" as the command that triggered it, even if not using transactions - in other words, it becomes somewhat part of that command.. Therefore, there is no risk of something changing the referenced id between inserts.