In a web game with PostgreSQL 9.3 backend I sometimes have to ban users, by putting them into the following table:
create table pref_ban (
id varchar(32) primary key,
first_name varchar(64),
last_name varchar(64),
city varchar(64),
last_ip inet,
reason varchar(128),
created timestamp default current_timestamp
);
I ban a user by running the following procedure, so that he/she can not enter the game at next login time (i.e. not the offender is not banned immediately):
create or replace function pref_delete_user(_id varchar,
_reason varchar) returns void as $BODY$
begin
insert into pref_ban --THIS FAILS WITH "duplicate key"
(id, first_name, last_name, city, last_ip, reason)
select id, first_name, last_name, city, last_ip, _reason
from pref_users where id = _id;
create temporary table temp_gids (gid int not null) on commit drop;
insert into temp_gids (gid) select gid from pref_scores where id=_id;
delete from pref_games p
using temp_gids t
where p.gid = t.gid;
create temporary table temp_rids (rid int not null) on commit drop;
insert into temp_rids (rid) select rid from pref_cards where id=_id;
delete from pref_rounds r
using temp_rids t
where r.rid = t.rid;
delete from pref_users where id=_id;
end;
$BODY$ language plpgsql;
However the INSERT statement in the above procedure sometimes fails with:
SQLSTATE[23505]: Unique violation: 7 ERROR: duplicate key value violates unique constraint "pref_ban_pkey" DETAIL: Key (id)=(GC1121680399) already exists.
CONTEXT: SQL statement "insert into pref_ban (id, first_name, last_name, city, last_ip, reason) select id, first_name, last_name, city, last_ip, _reason from pref_users where id = _id" PL/pgSQL function pref_delete_user(character varying,character varying) line 4 at SQL statement
This happens when some game stats have been written after I have banned the user for the 1st time (because users are not banned immediately and game stats are sometimes still being written into the database).
This is okay for me, but I wonder, how could I ignore the INSERT failure?
I know that typically the following "UPSERT" scheme is being used with PostgreSQL:
update pref_ban set
first_name = _first_name,
last_name = _last_name,
city = _city,
last_ip = _last_ip,
...
where id = _id;
if not found then
insert into pref_ban(id,
first_name,
last_name,
city,
last_ip,
...)
values (_id,
_first_name,
_last_name,
_city,
_last_ip,
...
now());
end if;
but this is NOT what I am after here: because I don't need to update the banned user details. I would just like to exit the procedure on the INSERT failure.
Related
Some background info: i have a table named defects which has column named status_id and another column named date_closed ,i want to set date_closed after status_id has been updated
i already try to do this using after update trigger with the following code:
after update on eba_bt_sw_defects
for each row
declare
l_status number(20) := null;
begin
select status_id into l_status from eba_bt_sw_defects D,eba_bt_status S where D.status_id = S.id;
if l_status in ( select id from eba_bt_status where is_open = 'N' and NVL(is_enhancement,'N')='N') then
:NEW.DATE_CLOSED := LOCALTIMESTAMP ;
end if;
end;
but an error occured ( subquery not allowed in this contextCompilation failed)
i want a help
A couple of things that need fixing in your code:
In a trigger do not select from the table the trigger you're on. This will probably raise a ORA-04091: table name is mutating, trigger/function may not see it error.
IF l_variable IN (SELECT ...) is not a valid oracle syntax. It raises PLS-00405: subquery not allowed in this context
I don't have your data so here is a similar example:
drop table todos;
drop table statuses;
-- create tables
create table statuses (
id number generated by default on null as identity
constraint statuses_id_pk primary key,
status varchar2(60 char),
is_open varchar2(1 char) constraint statuses_is_open_ck
check (is_open in ('Y','N'))
)
;
create table todos (
id number generated by default on null as identity
constraint todos_id_pk primary key,
name varchar2(255 char) not null,
close_date timestamp with local time zone,
status_id number
constraint todos_status_id_fk
references statuses on delete cascade
)
;
-- load data
insert into statuses (id, status, is_open ) values (1, 'OPEN', 'Y' );
insert into statuses (id, status, is_open ) values (2, 'COMPLETE', 'N' );
insert into statuses (id, status, is_open ) values (3, 'ON HOLD', 'Y' );
insert into statuses (id, status, is_open ) values (4, 'CANCELLED', 'N' );
commit;
insert into todos (name, close_date, status_id ) values ( 'Y2 Security Review', NULL, 1 );
-- triggers
CREATE OR REPLACE TRIGGER todos_biu BEFORE
INSERT OR UPDATE ON todos
FOR EACH ROW
DECLARE
l_dummy NUMBER;
BEGIN
SELECT
1
INTO l_dummy
FROM
statuses
WHERE
is_open = 'N' AND
id = :new.status_id;
:new.close_date := localtimestamp;
EXCEPTION
WHEN no_data_found THEN
-- I'm assuming you want close_date to NULL if todo is re-opened.
:new.close_date := NULL;
END todos_biu;
/
update todos set status_id = 2;
select * from todos;
id name close_date status_id
1 Y2 Security Review 11-MAY-22 05.27.04.987117000 PM 2
I have 2 tables like this
drop table if exists public.table_1;
drop table if exists public.table_2;
CREATE TABLE public.table_1 (
id serial NOT NULL,
user_id bigint not null,
status varchar(255) not null,
date_start date NOT NULL,
date_end date NULL
);
CREATE TABLE public.table_2 (
id serial NOT NULL,
user_id bigint not null,
status varchar(255) not null,
date_start date NOT NULL,
date_end date NULL
);
alter table public.table_1
add constraint my_constraint_1
EXCLUDE USING gist (user_id with =, daterange(date_start, date_end, '[]') WITH &&)
where (status != 'deleted');
alter table public.table_2
add constraint my_constraint_2
EXCLUDE USING gist (user_id with =, daterange(date_start, date_end, '[]') WITH &&)
where (status != 'deleted');
Every table contains rows which are related to a user, and all the rows of the same user cannot overlap in range. In addition, some rows may be logically deleted, so I added a where condition.
So far it's working w/o problems, but the 2 constraints work separately for each table.
I need to create a constraint which cover the 2 set of tables, so that a single daterange (of the same user and not deleted), may appaer only once across the 2 different tables.
Does the EXCLUDE notation be extended to work with different tables or do I need to check it with a trigger? If the trigger is the answer, which is the simplier way to do this? Create a temporary table with the union of the 2, add the constraint on it and check if fails?
Starting from #Laurenz Albe suggestion, this is what I made
-- #################### SETUP SAMPLE TABLES ####################
drop table if exists public.table_1;
drop table if exists public.table_2;
CREATE TABLE public.table_1 (
id serial NOT NULL,
user_id bigint not null,
status varchar(255) not null,
date_start date NOT NULL,
date_end date NULL
);
CREATE TABLE public.table_2 (
id serial NOT NULL,
user_id bigint not null,
status varchar(255) not null,
date_start date NOT NULL,
date_end date NULL
);
alter table public.table_1
add constraint my_constraint_1
EXCLUDE USING gist (user_id with =, daterange(date_start, date_end, '[]') WITH &&)
where (status != 'deleted');
alter table public.table_2
add constraint my_constraint_2
EXCLUDE USING gist (user_id with =, daterange(date_start, date_end, '[]') WITH &&)
where (status != 'deleted');
-- #################### SETUP TRIGGER ####################
create or REPLACE FUNCTION check_date_overlap_trigger_hook()
RETURNS trigger as
$body$
DECLARE
l_table text;
l_sql text;
l_row record;
begin
l_table := TG_ARGV[0];
l_sql := format('
select *
from public.%s as t
where
t.user_id = %s -- Include only records of the same user
and t.status != ''deleted'' -- Include only records that are active
', l_table, new.user_id);
for l_row in execute l_sql
loop
IF daterange(l_row.date_start, COALESCE(l_row.date_end, 'infinity'::date)) && daterange(new.date_start, COALESCE(new.date_end, 'infinity'::date))
THEN
RAISE EXCEPTION 'Date interval is overlapping with another one in table %', l_table
USING HINT = 'You can''t have the same interval across table1 AND table2';
END IF;
end loop;
RETURN NEW;
end
$body$
LANGUAGE plpgsql;
-- #################### INSTALL TRIGGER ####################
create trigger check_date_overlap
BEFORE insert or update
ON public.table_1
FOR EACH row
EXECUTE PROCEDURE check_date_overlap_trigger_hook('table_2');
create trigger check_date_overlap
BEFORE insert or update
ON public.table_2
FOR EACH row
EXECUTE PROCEDURE check_date_overlap_trigger_hook('table_1');
-- #################### INSERT DEMO ROWS ####################
insert into public.table_1 (user_id, status, date_start, date_end) values (1, 'active', '2020-12-10', '2020-12-20');
insert into public.table_1 (user_id, status, date_start, date_end) values (1, 'deleted', '2020-12-15', '2020-12-25');
insert into public.table_1 (user_id, status, date_start, date_end) values (2, 'active', '2020-12-10', '2020-12-20');
insert into public.table_1 (user_id, status, date_start, date_end) values (2, 'deleted', '2020-12-15', '2020-12-25');
-- This will fail for overlap on the same table
-- insert into public.table_1 (user_id, status, date_start, date_end) values (1, 'active', '2020-12-15', '2020-12-25');
-- This will fail as the user 1 already has an overlapping period on table 1
-- insert into public.table_2 (user_id, status, date_start, date_end) values (1, 'active', '2020-12-15', '2020-12-25');
-- This will fail as the user 1 already has an overlapping period on table 1
insert into public.table_2 (user_id, status, date_start, date_end) values (1, 'deleted', '2020-12-15', '2020-12-25');
update public.table_2 set status = 'active' where id = 1;
select 'table_1' as src_table, * from public.table_1
union
select 'table_2', * from public.table_2
You can probably use a trigger, but triggers are always vulnerable to race conditions (unless you are using SERIALIZABLE isolation).
If your tables really have the same columns, why don't you use a single table (and perhaps add a type column to disambiguate)?
I have a table people with columns: first_name, last_name, phone_number, address. Is it possible to have a function, that on insert checks if phone_number already exists and if so, it inserts existing values (first_name and last_name) instead of what user tried to insert? It would allow new address to be insert, only existing first_name and last_name would be copied. It is quite opposite to upsert, which would update the existing row. None of these columns have constraints.
I tried to use IF EXISTS, but I actually don't know how to pass the existing values into NEW. It works with fixed values, e.g. THEN NEW.first_name = 'Jack'.
BEGIN
IF EXISTS (SELECT 1 FROM people p WHERE p.phone_number = NEW.phone_number) THEN
NEW.first_name = ?;
END IF;
RETURN NEW;
I think you are looking for
INSERT INTO people(phone_number, first_name, last_name, address)
VALUES ($1,
COALESCE((SELECT first_name FROM people WHERE phone_number = $1), $2),
COALESCE((SELECT last_name FROM people WHERE phone_number = $1), $3),
$4);
I have a table with records which can reference another row in the same table so there is a parent-child relationship between rows in the same table.
What I am trying to achieve is to create the same data for another user so that they can see and manage their own version of this structure through the web ui where these rows are displayed as a tree.
Problem is when I bulk insert this data by only changing user_id, I lose the relation between rows because the parent_id values will be invalid for these new records and they should be updated as well with the newly generated ids.
Here is what I tried: (did not work)
Iterate over main_table
copy-paste the static values after each
do another insert on a temp table for holding old and new ids
update old parent_ids with new ids after loop ends
My attempt at doing such thing(last step is not included here)
create or replace function test_x()
returns void as
$BODY$
declare
r RECORD;
userId int8;
rowPK int8;
begin
userId := (select 1)
create table if not exists id_map (old_id int8, new_id int8);
create table if not exists temp_table as select * from main_table;
for r in select * from temp_table
loop
rowPK := insert into main_table(id, user_id, code, description, parent_id)
values(nextval('hibernate_sequence'), userId, r.code, r.description, r.parent_id) returning id;
insert into id_map (old_id, new_id) values (r.id, rowPK);
end loop;
end
$BODY$
language plpgsql;
My PostgreSQL version is 9.6.14.
DDL below for testing.
create table main_table(
id bigserial not null,
user_id int8 not null,
code varchar(3) not null,
description varchar(100) not null,
parent_id int8 null,
constraint mycompkey unique (user_id, code, parent_id),
constraint mypk primary key (id),
constraint myfk foreign key (parent_id) references main_table(id)
);
insert into main_table (id, user_id, code, description, parent_id)
values(0, 0, '01', 'Root row', null);
insert into main_table (id, user_id, code, description, parent_id)
values(1, 0, '001', 'Child row 1', 0);
insert into main_table (id, user_id, code, description, parent_id)
values(2, 0, '002', 'Child row 2', 0);
insert into main_table (id, user_id, code, description, parent_id)
values(3, 0, '002', 'Grand child row 1', 2);
How to write a procedure to accomplish this?
Thanks in advance.
It appears your task is coping all data for a given user to another while maintaining the hierarchical relationship within the new rows. The following accomplishes that.
It begins creating a new copy of the existing rows with the new user_id, including the old row parent_id. That will be user in the next (update) step.
The CTE logically begins with the new rows which have parent_id and joins to the old parent row. From here it joins to the old parent row to the new parent row using the code and description. At that point we have the new id along with the new parent is. At that point just update with those values. Actually for the update the CTE need only select those two columns, but I've left the intermediate columns so you trace through if you wish.
create or replace function copy_user_data_to_user(
source_user_id bigint
, target_user_id bigint
)
returns void
language plpgsql
as $$
begin
insert into main_table ( user_id,code, description, parent_id )
select target_user_id, code, description, parent_id
from main_table
where user_id = source_user_id ;
with n_list as
(select mt.id, mt.code, mt.description, mt.parent_id
, mtp.id p_id,mtp.code p_code,mtp.description p_des
, mtc.id c_id, mtc.code c_code, mtc.description c_description
from main_table mt
join main_table mtp on mtp.id = mt.parent_id
join main_table mtc on ( mtc.user_id = target_user_id
and mtc.code = mtp.code
and mtc.description = mtp.description
)
where mt.parent_id is not null
and mt.user_id = target_user_id
)
update main_table mt
set parent_id = n_list.c_id
from n_list
where mt.id = n_list.id;
return;
end ;
$$;
-- test
select * from copy_user_data_to_user(0,1);
select * from main_table;
CREATE TABLE 'table name you want to create' SELECT * FROM myset
but new table and myset column name should be equal and you can also
use inplace of * to column name but column name exist in new table
othwerwise getting errors
I am trying to create a trigger that when a user inserts into a table, the trigger will join with another table and insert into the primary one. Here is the trigger I have created so far. When I insert it into the database I get "Warning: Trigger Created with Compilation Errors."
CREATE OR REPLACE TRIGGER TBI_PERSONS_IDM_STAGING
AFTER INSERT
ON PERSONS_IDM_STAGING
FOR EACH ROW
BEGIN
IF :new.PERSON NOT IN (SELECT PERSON FROM PERSONS) AND INSERTING
THEN
INSERT INTO PERSONS (
PERSON,
DBLOGIN_ID,
LAST_NAME,
FIRST_NAME,
DEPARTMENT,
JOBTITLE,
USERLEVEL,
FACILITY_ID,
USER_OPTIONS,
PASSWORD)
VALUES(:new.PERSON,
:new.PERSON,
:new.LAST_NAME,
:new.FIRST_NAME,
:new.DEPARTMENT,
:new.JOBTITLE,
:new.USERLEVEL,
(SELECT KMS_FACILITY_ID FROM FACILITY_IDM_STAGING WHERE FACILITY_IDM_STAGING.IDM_CODE = :new.IDM_CODE),
:new.USER_OPTIONS,
:new.PASSWORD);
END IF;
END;
/
Can anyone see what I'm doing wrong?
Update: I was able to fix the code. Here is the correct statement if anyone else has this issue:
CREATE OR REPLACE TRIGGER TAI_PERSONS_IDM_STAGING
AFTER INSERT
ON PERSONS_IDM_STAGING
FOR EACH ROW
BEGIN
INSERT INTO PERSONS (
PERSON,
DBLOGIN_ID,
LAST_NAME,
FIRST_NAME,
DEPARTMENT,
JOBTITLE,
USERLEVEL,
FACILITY_ID,
USER_OPTIONS,
PASSWORD)
VALUES(:new.PERSON,
:new.PERSON,
:new.LAST_NAME,
:new.FIRST_NAME,
:new.DEPARTMENT,
:new.JOBTITLE,
:new.USERLEVEL,
(SELECT KMS_FACILITY_ID FROM FACILITY_IDM_STAGING WHERE FACILITY_IDM_STAGING.IDM_CODE = :new.IDM_CODE),
:new.USER_OPTIONS,
:new.PASSWORD);
EXCEPTION
WHEN DUP_VAL_ON_INDEX THEN NULL;
END;
/