Sqlite trigger oddly fails on unique constraint - triggers

This is SQLite version 3.16.0 2016-11-04
Here's my test schema:
PRAGMA foreign_keys = ON;
CREATE TABLE A (a_id integer primary key, x int);
CREATE TABLE B (b_id integer primary key, a_id integer not null references A(a_id) ON DELETE RESTRICT ON UPDATE CASCADE, descr text);
CREATE TABLE C (id integer primary key, dt DATETIME DEFAULT CURRENT_TIMESTAMP);
CREATE TRIGGER Tb after update on B begin
replace into C (id) select NEW.b_id;
end;
Here's some test data (as per .dump):
INSERT INTO "A" VALUES(1, 5000);
INSERT INTO "B" VALUES(1, 1, 'none');
Now, if I manually update a row in table B, like so:
UPDATE B SET descr = "test1" WHERE b_id = 1;
I can see a new row (which gets updated every time I touch B) in table C:
1|2017-04-17 21:59:42
I can also manually "touch" C the same way it's done in the trigger:
replace into C (id) select 1;
As expected, the dt column gets updated. All good so far.
But suddenly, if I change a_id, referenced from B:
UPDATE A SET a_id = 2 WHERE a_id = 1;
I get Error: UNIQUE constraint failed: C.id
In my understanding, the foreign key from B.a_id to A.a_id updates B's row. That leads to the trigger trying to touch C. Which then for some reason fails, though it just worked perfectly fine in previous scenarios.
Why does that happen and how can I fix it?

You are using the REPLACE command, which
is an alias for the "INSERT OR REPLACE" variant of the INSERT command.
The trigger documentation says:
An ON CONFLICT clause may be specified as part of an UPDATE or INSERT action within the body of the trigger. However if an ON CONFLICT clause is specified as part of the statement causing the trigger to fire, then conflict handling policy of the outer statement is used instead.
So when the outer command is an UPDATE, your REPLACE also becomes an UPDATE.
As a workaround, replace the REPLACE with the appropriate DELETE and INSERT commands.

Related

how to update and then insert a record with unique key constraint

i have a table with columns a,b,c and c having the unique key constraint.
a |b |c(uk)
----------
aa 1 z
I want to insert a new row with values (bb,1,z), if z already exists then i first want to update the existing row to
a |b |c(uk)
----------
aa 1 null
and then insert (bb,1,z), so that the final rows looks as shown below.
a |b |c(uk)
----------
aa 1 null
bb 1 z
how can i do this a single sql statement?
Unfortunately unique constraints are checked per row in Postgres - unless they are created as "deferrable".
So if you want to do the UPDATE and the INSERT in a single statement, you will have to re-create your unique constraint with the attribute deferrable. Then Postgres will check it at the end of the statement.
create table the_table
(
a text,
b int,
c text
);
alter table the_table
add constraint unique_c
unique(c)
deferrable --<< this is the "magic"
;
Then you can do it with a data modifying CTE
with input (a,b,c) as (
values ('bb', 1, 'z')
), change_old as (
update the_table
set c = null
from input
where input.c = the_table.c
)
insert into the_table
select a,b,c
from input;
I think you should use BEFORE INSERT TRIGGER for this.
In the trigger you would update all the records containing value new.c like:
UPDATE my_table SET
c = NULL
WHERE
c = new.c;
If c is not present - nothing will happen.
You may still run into unique constraint violation error if 2 concurent transactions inserts the same value into c. To avoid that you can use advisory locks providing lock ID from value of c.

Partial Unique Index not working with on conflict clause PostgreSQL

Table Structure:
create table example (a_id integer, b_id integer, c_id integer, flag integer);
Partial Index:
create unique index u_idx on example (a_id, b_id, c_id) where flag = 0;
My Code for upsert:
with a_ins_upd as (
Insert into example (a_id, b_id, c_id, flag)
select x.a_id, x.b_id, x.c_id, x.flag
from <input_tableType> x
on conflict (a_id, b_id, c_id) where flag = 0
do update
set
a_id = excluded.a_id,
b_id = excluded.b_id,
c_id = excluded.c_id,
flag = excluded.flag
returning *
)
insert into <some_other_table>
Result Entries, as of now :
Entry_1 is the data already in the table. Entry_2 , is the new input. The new input should update the Entry_1, rather than Inserting a new entry.
Where I am going wrong. I want the second entry to get updated, not Inserted.
I tried to include the flag as a part of the index. Then also, records are getting inserted. Does partial index not work with on conflict() clause?
How do I achieve this - I want to ignore the records with flag = 1, existing in the table (like blind to flag = 1), and do the merge operations on other records (the records which are having flag = 0, unique constraint remains on (a_id, b_id, c_id)). I thought of doing partial constraint/index and do on conflict. Seems tricky now.

After using a trigger - ERROR: null value in column "group_id" violates not-null constraint

I'm using PostgreSQL 8.1.23 on x86_64-redhat-linux-gnu
I have to write a database for reserving seats on language courses and there's a requirement there should be a trigger, which will check whether lector, we're trying to write into new group, has any other group at the same time. I have such table:
CREATE TABLE groups (
group_id serial PRIMARY KEY,
lang varchar(3) NOT NULL,
level varchar(3),
seats int4,
lector int4,
start time,
day varchar(3),
FOREIGN KEY (language) REFERENCES languages(lang) ON UPDATE CASCADE ON DELETE CASCADE,
FOREIGN KEY (lector) REFERENCES lectors(lector_id) ON UPDATE CASCADE ON DELETE SET NULL);
and such trigger:
CREATE FUNCTION if_available () RETURNS trigger AS '
DECLARE
r groups%rowtype;
c groups%rowtype;
BEGIN
FOR r IN SELECT * FROM groups WHERE r.lector=NEW.lector ORDER BY group_id LOOP
IF (r.start = NEW.start AND r.day = NEW.day) THEN
RAISE NOTICE ''Lector already has a group at this time!'';
c = NULL;
EXIT;
ELSE
c = NEW;
END IF;
END LOOP;
RETURN c;
END;
' LANGUAGE 'plpgsql';
CREATE TRIGGER if_available_t
BEFORE INSERT OR UPDATE ON grupy
FOR EACH ROW EXECUTE PROCEDURE if_available();
After inserting the new row to a table groups, eg.:
INSERT groups (lang, level, seats, lector, start, day) values ('ger','A-2',12,2,'11:45','wed');
I get an error like this:
ERROR: null value in column "group_id" violates not-null constraint
Without this trigger everything is OK. Could anybody help me how to make it work?
Finally, I have solved it! After BEGIN there should be c = NEW;, because when table groups is empty at the beginning, FOR loop doesn't run and NULL is returned. Also I have changed the condition in FOR loop for: ...WHERE lector = NEW.lector.... And finally, I have changed the condition in IF for IF (r.group_id <> NEW.group_id AND r.start = NEW.start AND r.day = NEW.day) THEN..., because I haven't wanted to run this trigger before one particular update. Maybe this will be helpful for someone :)

Foreign key constraints involving multiple tables

I have the following scenario in a Postgres 9.3 database:
Tables B and C reference Table A.
Table C has an optional field that references table B.
I would like to ensure that for each row of table C that references table B, c.b.a = c.a. That is, if C has a reference to B, both rows should point at the same row in table A.
I could refactor table C so that if c.b is specified, c.a is null but that would make queries joining tables A and C awkward.
I might also be able to make table B's primary key include its reference to table A and then make table C's foreign key to table B include table C's reference to table A but I think this adjustment would be too awkward to justify the benefit.
I think this can be done with a trigger that runs before insert/update on table C and rejects operations that violate the specified constraint.
Is there a better way to enforce data integrity in this situation?
There is a very simple, bullet-proof solution. Works for Postgres 9.3 - when the original question was asked. Works for the current Postgres 13 - when the question in the bounty was added:
Would like information on if this is possible to achieve without database triggers
FOREIGN KEY constraints can span multiple columns. Just include the ID of table A in the FK constraint from table C to table B. This enforces that linked rows in B and C always point to the same row in A. Like:
CREATE TABLE a (
a_id int PRIMARY KEY
);
CREATE TABLE b (
b_id int PRIMARY KEY
, a_id int NOT NULL REFERENCES a
, UNIQUE (a_id, b_id) -- redundant, but required for FK
);
CREATE TABLE c (
c_id int PRIMARY KEY
, a_id int NOT NULL REFERENCES a
, b_id int
, CONSTRAINT fk_simple_and_safe_solution
FOREIGN KEY (a_id, b_id) REFERENCES b(a_id, b_id) -- THIS !
);
Minimal sample data:
INSERT INTO a(a_id) VALUES
(1)
, (2);
INSERT INTO b(b_id, a_id) VALUES
(1, 1)
, (2, 2);
INSERT INTO c(c_id, a_id, b_id) VALUES
(1, 1, NULL) -- allowed
, (2, 2, 2); -- allowed
Disallowed as requested:
INSERT INTO c(c_id, a_id, b_id) VALUES (3,2,1);
ERROR: insert or update on table "c" violates foreign key constraint "fk_simple_and_safe_solution"
DETAIL: Key (a_id, b_id)=(2, 1) is not present in table "b".
db<>fiddle here
The default MATCH SIMPLE behavior of FK constraints works like this (quoting the manual):
MATCH SIMPLE allows any of the foreign key columns to be null; if any of them are null, the row is not required to have a match in the referenced table.
So NULL values in c(b_id) are still allowed (as requested: "optional field"). The FK constraint is "disabled" for this special case.
We need the logically redundant UNIQUE constraint on b(a_id, b_id) to allow the FK reference to it. But by making it out to be on (a_id, b_id) instead of (b_id, a_id), it is also useful in its own right, providing a useful index on b(a_id) to support the other FK constraint, among other things. See:
Is a composite index also good for queries on the first field?
(An additional index on c(a_id) is typically useful accordingly.)
Further reading:
Differences between MATCH FULL, MATCH SIMPLE, and MATCH PARTIAL?
Enforcing constraints “two tables away”
I ended up creating a trigger as follows:
create function "check C.A = C.B.A"()
returns trigger
as $$
begin
if NEW.b is not null then
if NEW.a != (select a from B where id = NEW.b) then
raise exception 'a != b.a';
end if;
end if;
return NEW;
end;
$$
language plpgsql;
create trigger "ensure C.A = C.B.A"
before insert or update on C
for each row
execute procedure "check C.A = C.B.A"();
Would like information on if this is possible to achieve without database triggers
Yes, it is possible. The mechanism is called ASSERTION and it is defined in SQL-92 Standard(though it is not implemented by any major RDBMS).
In short it allows to create multiple-row constraints or multi-table check constraints.
As for PostgreSQL it could be emulated by using view with WITH CHECK OPTION and performing operation on view instead of base table.
WITH CHECK OPTION
This option controls the behavior of automatically updatable views. When this option is specified, INSERT and UPDATE commands on the view will be checked to ensure that new rows satisfy the view-defining condition (that is, the new rows are checked to ensure that they are visible through the view). If they are not, the update will be rejected.
Example:
CREATE TABLE a(id INT PRIMARY KEY, cola VARCHAR(10));
CREATE TABLE b(id INT PRIMARY KEY, colb VARCHAR(10), a_id INT REFERENCES a(id) NOT NULL);
CREATE TABLE c(id INT PRIMARY KEY, colc VARCHAR(10),
a_id INT REFERENCES a(id) NOT NULL,
b_id INT REFERENCES b(id));
Sample inserts:
INSERT INTO a(id, cola) VALUES (1, 'A');
INSERT INTO a(id, cola) VALUES (2, 'A2');
INSERT INTO b(id, colb, a_id) VALUES (12, 'B', 1);
INSERT INTO c(id, colc, a_id) VALUES (15, 'C', 2);
Violating the condition(connecting C with B different a_id on both tables)
UPDATE c SET b_id = 12 WHERE id = 15;;
-- no issues whatsover
Creating view:
CREATE VIEW view_c
AS
SELECT *
FROM c
WHERE NOT EXISTS(SELECT 1
FROM b
WHERE c.b_id = b.id
AND c.a_id != b.a_id) -- here is the clue, we want a_id to be the same
WITH CHECK OPTION ;
Trying update second time(error):
UPDATE view_c SET b_id = 12 WHERE id = 15;
--ERROR: new row violates check option for view "view_c"
--DETAIL: Failing row contains (15, C, 2, 12).
Trying brand new inserts with incorrect data(also errors)
INSERT INTO b(id, colb, a_id) VALUES (20, 'B2', 2);
INSERT INTO view_c(id, colc, a_id, b_id) VALUES (30, 'C2', 1, 20);
--ERROR: new row violates check option for view "view_c"
--DETAIL: Failing row contains (30, C2, 1, 20)
db<>fiddle demo

Get row to swap tables on a certain condition

I currently have a parent table:
CREATE TABLE members (
member_id SERIAL NOT NULL, UNIQUE, PRIMARY KEY
first_name varchar(20)
last_name varchar(20)
address address (composite type)
contact_numbers varchar(11)[3]
date_joined date
type varchar(5)
);
and two related tables:
CREATE TABLE basic_member (
activities varchar[3])
INHERITS (members)
);
CREATE TABLE full_member (
activities varchar[])
INHERITS (members)
);
If the type is full the details are entered to the full_member table or if type is basic into the basic_member table. What I want is that if I run an update and change the type to basic or full the tuple goes into the corresponding table.
I was wondering if I could do this with a rule like:
CREATE RULE tuple_swap_full
AS ON UPDATE TO full_member
WHERE new.type = 'basic'
INSERT INTO basic_member VALUES (old.member_id, old.first_name, old.last_name,
old.address, old.contact_numbers, old.date_joined, new.type, old.activities);
... then delete the record from the full_member
Just wondering if my rule is anywhere near or if there is a better way.
You don't need
member_id SERIAL NOT NULL, UNIQUE, PRIMARY KEY
A PRIMARY KEY implies UNIQUE NOT NULL automatically:
member_id SERIAL PRIMARY KEY
I wouldn't use hard coded max length of varchar(20). Just use text and add a check constraint if you really must enforce a maximum length. Easier to change around.
Syntax for INHERITS is mangled. The key word goes outside the parens around columns.
CREATE TABLE full_member (
activities text[]
) INHERITS (members);
Table names are inconsistent (members <-> member). I use the singular form everywhere in my test case.
Finally, I would not use a RULE for the task. A trigger AFTER UPDATE seems preferable.
Consider the following
Test case:
Tables:
CREATE SCHEMA x; -- I put everything in a test schema named "x".
-- DROP TABLE x.members CASCADE;
CREATE TABLE x.member (
member_id SERIAL PRIMARY KEY
,first_name text
-- more columns ...
,type text);
CREATE TABLE x.basic_member (
activities text[3]
) INHERITS (x.member);
CREATE TABLE x.full_member (
activities text[]
) INHERITS (x.member);
Trigger function:
Data-modifying CTEs (WITH x AS ( DELETE ..) are the best tool for the purpose. Requires PostgreSQL 9.1 or later.
For older versions, first INSERT then DELETE.
CREATE OR REPLACE FUNCTION x.trg_move_member()
RETURNS trigger AS
$BODY$
BEGIN
CASE NEW.type
WHEN 'basic' THEN
WITH x AS (
DELETE FROM x.member
WHERE member_id = NEW.member_id
RETURNING *
)
INSERT INTO x.basic_member (member_id, first_name, type) -- more columns
SELECT member_id, first_name, type -- more columns
FROM x;
WHEN 'full' THEN
WITH x AS (
DELETE FROM x.member
WHERE member_id = NEW.member_id
RETURNING *
)
INSERT INTO x.full_member (member_id, first_name, type) -- more columns
SELECT member_id, first_name, type -- more columns
FROM x;
END CASE;
RETURN NULL;
END;
$BODY$
LANGUAGE plpgsql VOLATILE;
Trigger:
Note that it is an AFTER trigger and has a WHEN condition.
WHEN condition requires PostgreSQL 9.0 or later. For earlier versions, you can just leave it away, the CASE statement in the trigger itself takes care of it.
CREATE TRIGGER up_aft
AFTER UPDATE
ON x.member
FOR EACH ROW
WHEN (NEW.type IN ('basic ','full')) -- OLD.type cannot be IN ('basic ','full')
EXECUTE PROCEDURE x.trg_move_member();
Test:
INSERT INTO x.member (first_name, type) VALUES ('peter', NULL);
UPDATE x.member SET type = 'full' WHERE first_name = 'peter';
SELECT * FROM ONLY x.member;
SELECT * FROM x.basic_member;
SELECT * FROM x.full_member;