SELECT on same table AFTER INSERT TRIGGER fails? - sql

I am writing a small AFTER INSERT trigger being executed after insert of a row in a table A.
So simplified we´ve got these three tables:
CREATE TABLE A (Id INTEGER, datum DATE, ...);
CREATE TABLE B (Id INTEGER, ...);
CREATE TABLE C (Id INTEGER, Aref INTEGER, Bref INTEGER, FOREIGN KEY(Aref) REFERENCES A(Id), FOREIGN KEY(Bref) REFERENCES B(Id));
So now the tricky thing is, that the rows out of table B should only be selected, if they are EITHER not referenced at all in table C OR, if they are referenced in table C, the datum field in the row of A also being referenced in this row of C IS NOT NULL.
You´re getting it?
So I tried my luck with a select clause looking the following:
SELECT * FROM B b WHERE NOT EXISTS(SELECT c.* FROM C c, A a WHERE c.Bref = b.Id AND c.Aref = A.Id and a.datum IS NULL);
So now the trigger is throwing an exception, because "the table is modified at the moment and the trigger could probably not see it" cited the error message. But I need the information of table A and I need it to be an AFTER INSERT Trigger cause the same trigger is using this rows for a calculation itself inserting rows in a table D with reference to this row of A.
So now the question is, how is it possible to select this rows of A after insert and make sure it keeps consistent?

Related

SQLite before insert trigger

I'm trying to insert duplicates data to table B when PKA violated using a BEFORE INSERT TRIGGER as the following example:
CREATE TABLE A( Col INTEGER, Coll TEXT(25), CONSTRAINT PKA PRIMARY KEY(Col, Coll) ON CONFLICT IGNORE);
CREATE UNIQUE INDEX IX_A ON A(Col, Coll);
CREATE TABLE B( Col INTEGER, Coll TEXT(25));
CREATE INDEX IX_B ON B(Col, Coll);
CREATE TRIGGER Trig
BEFORE INSERT
ON A
WHEN (Col = New.Col AND Coll = New.Coll)
BEGIN
INSERT INTO B(Col, Coll) VALUES(New.Col, New.Coll);
END;
But, it seems like the column Col is not accessible there, so it throws:
no such column: Col
Even when I change the conditions to
New.Col IN(SELECT Col FROM A)
AND
New.Coll IN(SELECT Coll FROM A)
I get another error message:
UNIQUE constraint failed: A.Col, A.Coll
While it shouldn't because of ON CONFLICT IGNORE.
Why did I get those error messages? (What's the cause).
How can I use the trigger to insert duplicates into another table?
You don't need the index:
CREATE UNIQUE INDEX IX_A ON A(Col, Coll);
because you have already defined (Col, Coll) as the PRIMARY KEY and furthermore, with this index, although you have ON CONFLICT IGNORE defined for a duplicate row, you will receive an error if you try to insert a duplicate row, because ON CONFLICT IGNORE is not defined for the index.
So drop it:
DROP INDEX IF EXISTS IX_A;
Now, change the code of the trigger to this:
CREATE TRIGGER Trig
BEFORE INSERT
ON A
WHEN EXISTS (SELECT 1 FROM A WHERE Col = New.Col AND Coll = New.Coll)
BEGIN
INSERT INTO B(Col, Coll) VALUES(New.Col, New.Coll);
END;
EXISTS checks the table A if it already contains a row with column values the same as the ones to be inserted and if it does then the new row is inserted in table B.
You could also write the trigger like this:
CREATE TRIGGER Trig
BEFORE INSERT
ON A
BEGIN
INSERT INTO B(Col, Coll)
SELECT Col, Coll FROM A
WHERE (Col, Coll) = (New.Col, New.Coll);
END;

Update each row of a table with the corresponding value

I have two Postgres tables:
create table A(
id_A serial not null,
column_A varchar null;
...);
create table B(
id_B serial not null,
id_A int4 not null,
name varchar null,
keywords varchar null,
...);
An element of table A is associated to multiple elements of table B and an element of table B is associated to one element of table A.
The column keywords in table B is a concatenation of values of columns B.name and A.column_A:
B.keywords := B.name || A.column_A
How to update with a trigger the column B.keywords of each row in table B if the value of A.column_A is updated?
In other words, I want to do something like this (pseudo-code):
FOR EACH ROW current_row IN TABLE B
UPDATE B SET keywords = (SELECT B.name || A.column_A
FROM B INNER JOIN A ON B.id_A = A.id_A
WHERE B.id_B = current_row.id_B)
WHERE id_B = current_row.id_B;
Your trigger has to call a function when A is updated:
CREATE OR REPLACE FUNCTION update_b()
RETURNS TRIGGER
AS $$
BEGIN
UPDATE B
SET keywords = name || NEW.column_A
WHERE id_A = NEW.id_A;
return NEW;
END
$$ LANGUAGE plpgsql;
CREATE TRIGGER update_b_trigger AFTER UPDATE OF column_A
ON A
FOR EACH ROW
EXECUTE PROCEDURE update_b();
It might also be useful to add a trigger BEFORE INSERT OR UPDATE on table B to set the keywords.
Your approach is broken by design. Do not try to keep derived values current in the table. That's not safe for concurrent access. All kinds of complications can arise. You bloat table B (and backups) and impair write performance.
Instead, use a VIEW (or a MATERIALIZED VIEW):
CREATE VIEW ab AS
SELECT B.*, concat_ws(', ', B.name, A.column_A) AS keywords
FROM B
LEFT JOIN A USING (id_A);
With the updated table definition below referential integrity is guaranteed and you can use [INNER] JOIN instead of LEFT [OUTER] JOIN.
Or even a simple query might be enough ...
Either way, you need a PRIMARY KEY constraint in table A and a FOREIGN KEY constraint in table B:
CREATE TABLE A (
id_A serial PRIMARY KEY,
column_A varchar
...);
CREATE TABLE B (
id_B serial PRIMARY KEY,
id_A int4 NOT NULL REFERENCES A(id_A),
name varchar
-- and *no* redundant "keywords" column!
...);
About concatenating strings:
How to concatenate columns in a Postgres SELECT?
And I wouldn't use CaMeL-case identifiers:
Are PostgreSQL column names case-sensitive?

Insert inserted id to another table

Here's the scenario:
create table a (
id serial primary key,
val text
);
create table b (
id serial primary key,
a_id integer references a(id)
);
create rule a_inserted as on insert to a do also insert into b (a_id) values (new.id);
I'm trying to create a record in b referencing to a on insertion to a table. But what I get is that new.id is null, as it's automatically generated from a sequence. I also tried a trigger AFTER insert FOR EACH ROW, but result was the same. Any way to work this out?
To keep it simple, you could also just use a data-modifying CTE (and no trigger or rule):
WITH ins_a AS (
INSERT INTO a(val)
VALUES ('foo')
RETURNING a_id
)
INSERT INTO b(a_id)
SELECT a_id
FROM ins_a
RETURNING b.*; -- last line optional if you need the values in return
Related answer with more details:
PostgreSQL multi INSERT...RETURNING with multiple columns
Or you can work with currval() and lastval():
How to get the serial id just after inserting a row?
Reference value of serial column in another column during same INSERT
Avoid rules, as they'll come back to bite you.
Use an after trigger on table a that runs for each row. It should look something like this (untested):
create function a_ins() returns trigger as $$
begin
insert into b (a_id) values (new.id);
return null;
end;
$$ language plpgsql;
create trigger a_ins after insert on a
for each row execute procedure a_ins();
Don't use triggers or other database Kung fu. This situation happens every moment somewhere in the world - there is a simple solution:
After the insertion, use the LASTVAL() function, which returns the value of the last sequence that was auto-incremented.
Your code would look like:
insert into a (val) values ('foo');
insert into b (a_id, val) values (lastval(), 'bar');
Easy to read, maintain and understand.

Create trigger that ensures there is one and only one NON-NULL in a set of column and reuse it for other tables

I know how to create a trigger that checks if a group of columns has one and only one NON NULL for one table but i would like to reuse the code because i will have some other tables with the same requirements. Any recommendations? I was thinking of maybe a trigger that passes it's name of the columns to be checked and table name to a stored procedure and the function does the rest, but i'm not sure on how to implement it.
EDIT: i tried
DROP tAble if exists t;
create table t(
a integer,
b integer,
c integer,
CONSTRAINT enforce_only1FK CHECK ((a <> NULL)::integer +(b <> NULL)::integer+(c <>NULL)::integer = 1)
);
INSERT into t VALUES (4,NULL,6);
it should not allow the insert but it does... what am i doing wrong?
EDIT 2 : interesting... it works if i write
DROP tAble if exists t;
create table t(
a integer,
b integer,
c integer,
CONSTRAINT enforce_only1FK CHECK ((a NOT NULL)::integer +(b NOT NULL)::integer+(c NOT NULL)::integer = 1)
);
INSERT into t VALUES (4,NULL,6);
a trigger that checks if a group of columns has one and only one NON
NULL for one table
This would be a case for a table-level check constraint rather than a trigger.
Example with the constraint on the first 3 columns:
CREATE TABLE tablename (
a int,
b int,
c int,
d text,
CHECK ((a is not null and b is null and c is null)
OR (a is null and b is not null and c is null)
OR (a is null and b is null and c is not null))
);
or in more elaborate form with a function:
CREATE FUNCTION count_notnull(variadic arr int[]) returns int as
$$
select sum(case when $1[i] is null then 0 else 1 end)::int
from generate_subscripts($1,1) a(i);
$$ language sql immutable;
CREATE TABLE tablename (
a int,
b int,
c int,
d text,
CHECK (count_notnull(a,b,c)=1)
);
This second form looks better when many columns are involved in the constraint but it requires them to be all of the same type.
That is not a case for a trigger. Just a check constraint:
create table t (
a integer,
b text,
c boolean
check ((
(a is not null)::integer
+ (b is not null)::integer
+ (c is not null)::integer
) = 1)
);]
In instead of checking every possible combination just use the boolean cast to integer and sum the results.
insert into t (a, b, c) values
(1, 'a', true);
ERROR: new row for relation "t" violates check constraint "t_check"
DETAIL: Failing row contains (1, a, t).
insert into t (a, b, c) values
(null, 'b', false);
ERROR: new row for relation "t" violates check constraint "t_check"
DETAIL: Failing row contains (null, b, f).
insert into t (a, b, c) values
(2, null, null);
INSERT 0 1
insert into t (a, b, c) values
(null, null, null);
ERROR: new row for relation "t" violates check constraint "t_check"
DETAIL: Failing row contains (null, null, null).
I am not sure, how much following trick is clean or ugly, but it works:
postgres=# create table foo1(a int);
CREATE TABLE
postgres=# create unique index on foo1((1)) where a is null;
CREATE INDEX
postgres=# insert into foo1 values(null);
INSERT 0 1
postgres=# insert into foo1 values(null);
ERROR: duplicate key value violates unique constraint "foo1_expr_idx"
DETAIL: Key ((1))=(1) already exists.
postgres=#
Notes to triggers: PL/pgSQL is not good language for writing generic triggers. You should to use PLperl or PLpython. Next - triggers is not good tool for ensuring uniqueness - you should be very careful there - there is a risk of race conditions or high locking - it require some experience do this work well. So better method is using indexes where is it possible.

Updating foreign keys while inserting into new table

I have table A(id).
I need to
create table B(id)
add a foreign key to table A that references to B.id
for every row in A, insert a row in B and update A.b_id with the newly inserted row in B
Is it possible to do it without adding a temporary column in B that refers to A? The below does work, but I'd rather not have to make a temporary column.
alter table B add column ref_id integer references(A.id);
insert into B (ref_id) select id from A;
update A set b_id = B.id from B where B.ref_id = A.id;
alter table B drop column ref_id;
Assuming that:
1) you're using postgresql 9.1
2) B.id is a serial (so actually an int with a default value of nextval('b_id_seq')
3) when inserting to B, you actually add other fields from A otherwise the insert is useless
...I think something like this would work:
with n as (select nextval('b_id_seq') as newbid,a.id as a_id from a),
l as (insert into b(id) select newbid from n returning id as b_id)
update a set b_id=l.b_id from l,n where a.id=n.a_id and l.b_id=n.newbid;
Add the future foreign key column, but without the constraint itself:
ALTER TABLE A ADD b_id integer;
Fill the new column with values:
WITH cte AS (
SELECT
id
ROW_NUMBER() OVER (ORDER BY id) AS b_ref
FROM A
)
UPDATE A
SET b_id = cte.b_ref
FROM cte
WHERE A.id = cte.id;
Create the other table:
CREATE TABLE B (
id integer CONSTRAINT PK_B PRIMARY KEY
);
Add rows to the new table using the referencing column of the existing one:
INSERT INTO B (id)
SELECT b_id
FROM A;
Add the FOREIGN KEY constraint:
ALTER TABLE A
ADD CONSTRAINT FK_A_B FOREIGN KEY (b_id) REFERENCES B (id);
PostgeSQL dialect.
You might use an anonymous code block like this
do $$
declare
category_cursor cursor for select id from schema1.categories;
r_category bigint;
setting_id bigint;
begin
open category_cursor;
loop fetch category_cursor into r_category;
exit when not found;
insert into schema2.setting(field)
values ('field_value') returning id into setting_id;
update schema1.categories set category_setting_id = setting_id
where category_id = r_category;
end loop;
end; $$
Let assume we have two tables first - categories, second - settings which must be applied to these categories.
First step - declare cursor(collect ids from categories), and variabels where we store temporary data
Loop cursor inserting values 'field_value' into settings
Store id in variable setting_id
Update table categories with setting_id