postgres - trigger before transaction commit - postgresql

I was wondering if it is indirectly possible to have a trigger executed just before the transaction is about to commit? In this trigger, I will do consistency checks and rollback the transaction if required.
For example, I have three tables:
users (id, name)
groups (id, name)
user_in_group (user_id, group_id)
I would like to create a trigger which verifies that a user is always part of a group. No orphan users are allowed. Each time an insert into users occurs, this trigger will verify that a correspondering insert into user_in_group also occured. If not, the transaction will not commit.
This cannot be done using a simple row- or statement- based trigger, since the above scenario requires two separate statements.
The other way around, when a delete from user_in_group happens, can be easily done by a row-based trigger.

Did you look into the CREATE CONSTRAINT TRIGGER, with the DEFERRABLE (INITIALLY DEFERRED) option?

Wouldn't the correct method really be to establish constraints into the database? That seems exactly what constraints are for. Add a foreign key constraint and a not null and it would seem you should be in business.
Now revised for symmetrical constraints:
drop table foousers cascade;
drop table foogroups cascade;
drop table foousergrps cascade;
create table foousers (id int primary key, name text);
create table foogroups (id int primary key, name text);
create table foousergrps (user_id int unique references foousers not null, group_id int unique references foogroups not null);
alter table foogroups add foreign key (id) references foousergrps (group_id) deferrable initially deferred;
alter table foousers add foreign key (id) references foousergrps (user_id) deferrable initially deferred;
begin;
insert into foousers values (0, 'root');
insert into foousers values (1, 'daemon');
insert into foogroups values (0, 'wheel');
insert into foogroups values (1, 'daemon');
insert into foousergrps values (0,0);
insert into foousergrps values (1,1);
commit;
Forbidden:
insert into foousers values (2, 'bad');
insert into foousergrps values (2,2);
Example of (non-deferrable, boo) check function:
create table foousergrps (user_id int unique references foousers not null, group_id int not null);
create function fooorphangroupcheck(int) returns boolean as $$
declare
gid alias for $1;
begin
perform 1 from foousergrps where group_id = gid limit 1;
if NOT FOUND then return false;
end if;
return true;
end;
$$
LANGUAGE 'plpgsql';
alter table foogroups add check (fooorphangroupcheck(id));

Lookind at the doc, there seems to be no such trigger option... so one way to achieve "no orphan users" rule would be to not allow direct insert into users and user_in_group tables. Instead create a view (which combines these tables, ie user_id, user_name, group_id) with a update rule which inserts data into right tables.
Or only allow inserting new users via stored procedure which takes all required data as inpud and thus doesn't allow users withoud group.
BTW, why do you have separate table for user and group relationship? Why not add group_id field into users table with FK / NOT NULL constraint?

From the docs . . .
Triggers can be defined to execute either before or after any INSERT,
UPDATE, or DELETE operation, either once per modified row, or once per
SQL statement.

You can use the sql WITH operator, like this:
WITH insert_user AS (
INSERT INTO users(name) VALUES ('bla-bla-user') RETURNING id
)
INSERT INTO user_in_group(user_id, group_id)
SELECT id, 999 FROM insert_user UNION
SELECT id, 888 FROM insert_user;
SELECT groups (id, name) user_in_group (user_id, group_id)

Related

Insert entries into a table with uniqueness check skipped

I have this table which has some uniqueness constraints and when I do a particular insert statement I want make sure that uniqueness constraints are disabled and not enabled..
Is possible to disable the unique constrains defined for a table and then enable it again - or perhaps even better only for a particular query?
I am using postgres version 12
My tables are initially created without any uniqueness constrains but are altered to have an uniqueness constrain if it need to have one..
-- Create table with EntityId
CREATE TABLE IF NOT EXISTS public.{table_name}
({table_name}_id BIGSERIAL PRIMARY KEY);
-- Create table_registration
CREATE TABLE IF NOT EXISTS public.{table_name}_registration
(
entry_id bigint REFERENCES |entityName|({table_name}_id),
row_id BIGSERIAL PRIMARY KEY,
valid tsrange,
entry_name text,
registration tsrange,
registration_by varchar(255),
|NewAttribute| |datatype|
);
Later are constrains added like this
ALTER TABLE {table_name}_registration
DROP Constraint IF EXISTS {table_name}_{string.Join('_', listOfAttributes)}_key,
ADD Constraint {table_name}_{string.Join('_', listOfAttributes)}_key UNIQUE({string.Join(',', listOfAttributes)});
Prior to doing this all uniqueness is dropped using this
-- Drop all uniqueness constraints
DO $$
DECLARE r RECORD;
BEGIN
FOR r in SELECT
conrelid::regclass,
conname
FROM
pg_constraint
WHERE
contype IN ('u')
AND connamespace = 'public'::regnamespace
LOOP
EXECUTE format('ALTER TABLE %I DROP CONSTRAINT %I', r.conrelid::text, r.conname::text);
END LOOP;
END;
$$

tSQLt will not apply misnamed constraints (PK_dbo ....) to fake table

Adding tSQLt tests to an existing production product, so we're not able to alter tables, constraints, etc. Currently all the constraints are labeled like 'PK_dbo.ViolationCategory' when they should be like 'PK_ViolationCategory'
When I run:
EXEC tSQLt.NewTestClass 'AdHocReportFiltersTestConstraint';
GO
CREATE PROCEDURE [AdHocReportFiltersTestConstraint].[Setup]
AS
BEGIN
EXEC tSQLt.FakeTable 'dbo.AdHocReports'
END
GO
CREATE PROCEDURE [AdHocReportFiltersTestConstraint].[test_AdHocReportFilters_Constraint]
AS
BEGIN
DECLARE #Id uniqueidentifier = NEWID()
DECLARE #Name NVARCHAR(Max) = 'Test_Name'
DECLARE #Value NVARCHAR(Max) = 'Test_Value'
DECLARE #AdHocReport_ID uniqueidentifier = NEWID()
INSERT INTO dbo.AdHocReportFilters ([Id], [Name], [Value], [AdHocReport_Id])
VALUES (#Id, #Name, #Value, #AdHocReport_ID)
exec tSQLt.ApplyConstraint 'dbo.AdHocReports', 'PK_dbo.AdHocReportFilters';
END
EXEC tSQLt.RunTestClass 'AdHocReportFiltersTestConstraint';
GO
I receive the error,
(1 row affected)
[AdHocReportFiltersTestConstraint].[test_AdHocReportFilters_Constraint] failed:
(Error) The INSERT statement conflicted with the FOREIGN KEY constraint
"FK_dbo.AdHocReportFilters_dbo.AdHocReports_AdHocReport_Id". The conflict occurred in
database "CR", table "dbo.tSQLt_tempobject_fbc9c8bf09e742929eccae914d5e440d",
column 'Id'.[16,0]{test_AdHocReportFilters_Constraint,11}
Any ideas of how to work around this?
Once I get this working, I will add a second record to violate the PK constraint and catch the error.
Just looking at your code, I see you are doing the following:
Fake the table dbo.AdHocReports
Insert a row into the table dbo.AdHocReportFilters
Apply a PK constraint called "PK_dbo.AdHocReportFilters" to the table dbo.AdHocReports
You will then try and add another row to validate the PK_dbo.AdHocReportFilters constraint
The error you are getting appears to to suggest that you are violating a foreign key on the AdHocReportFilters table - which is expected since that table hasn't been faked.
It is not clear from the test name whether you are trying to validate the behaviour of the primary key or foreign key.
Looking at the steps, I think you may be mixing up the two tables but without more detailed code (i.e. CREATE TABLE) statements, it is difficult for me to help you further.
There are two tables:
dbo.AdHocReports (Id,... (more columns))
dbo.AdHocReportFilters ([Id], [Name], [Value], [AdHocReport_Id] )
AdHocReportFilters.AdHocReport_Id is foriegn key for table dbo.AdHocReportFilters referencing dbo.AdHocReports.Id
When you are inserting insert a row in table AdHocReportFilters, you should make sure AdHocReportFilters.AdHocReport_Id is an id column in table dbo.AdHocReports
But in your SP, #AdHocReport_ID is NEWID(), which is of course a unique value and hence not in table dbo.AdHocReports.Id
Work around,
Disable foreign key contraint
ALTER TABLE dbo.AdHocReportFilters NOCHECK CONSTRAINT FK_dbo.AdHocReportFilters_dbo.AdHocReports_AdHocReport_Id

How to get child table's foreign key to have same value as parent's primary auto-increment key

I have created these two tables:
CREATE TABLE Purchase(
purchaseID SERIAL,
custName VARCHAR(30) NOT null,
PRIMARY KEY (purchaseID));
CREATE TABLE PurchasedItem(
purchaseID INT,
itemNo INT NOT NULL,
PRIMARY KEY (purchaseID, itemNo),
FOREIGN KEY (purchaseID) REFERENCES Purchase(purchaseID));
Next I wish to insert data into both tables, with the purchaseID foreign key of purchased item having the same value as the purchaseID Serial from Purchase table.
I am using a PostgreSQL client called PSequel. I tried setting AUTOCOMMIT to off first in the client so I could have the two INSERT statement in the same transaction, however the client didn't recognise "autocommit", so I tried it in the terminal and I think it worked... anyway, these are the two INSERT statements I tried.
INSERT INTO Purchase(custName) VALUES ('Lendl');
INSERT INTO PurchasedItem(purchaseID, itemNo) VALUES (DEFAULT, 111);
commit;
However I get an error:
ERROR: null value in column purchaseID violates not-null constraint.
this is referring to the PurchasedItem's purchaseID as in running the first INSERT statement by itself it works. How do I solve this problem?
DEFAULT will work for SERIAL as it sets default value for column. So
INSERT INTO Purchase VALUES (DEFAULT,'Lendl');
should work. But PurchasedItem.purchaseID has no default value set, so it it tries to insert NULL (and null is not in referenced column yet), so it fails.
try:
INSERT INTO Purchase(custName) VALUES ('Lendl') RETURNING purchaseID;
you will see the value of inserted purchaseID, use it in next query:
INSERT INTO PurchasedItem(purchaseID, itemNo) VALUES (_the_value_above_, 111);
commit;
If you want it to be used without interactivity, use DO block with returning purchaseID into _value
update:
or cte, smth like
WITH i AS (
INSERT INTO Purchase(custName, orderedDate)
VALUES ('Lendl', '2016-09-28')
RETURNING purchaseID
)
insert into PurchasedItem
select i.purchaseID,'smth',3
from i
You can use lastval()
INSERT INTO Purchase(custName) VALUES ('Lendl');
INSERT INTO PurchasedItem(purchaseID, itemNo) VALUES (lastval(), 111);
commit;
Alternatively query the underlying sequence directly:
INSERT INTO Purchase(custName) VALUES ('Lendl');
INSERT INTO PurchasedItem(purchaseID, itemNo)
VALUES (currval('purchase_purchaseid_seq'), 111);
commit;
Or if you don't want to rely on the automatic naming of the sequence, use pg_get_serial_sequence to get the sequence associated with the column:
INSERT INTO Purchase(custName) VALUES ('Lendl');
INSERT INTO PurchasedItem(purchaseID, itemNo)
VALUES (currval(pg_get_serial_sequence('purchase', 'purchaseid')), 111);
commit;
For more details see the manual: https://www.postgresql.org/docs/current/static/functions-sequence.html

Set column as primary key if the table doesn't have a primary key

I have a column in db which has 5 columns but no primary key.
One of the columns is named myTable_id and is integer.
I want to check if the table has a primary key column. If it doesn't, then make myTable_id a primary key column and make it identity column. Is there a way to do this?
I tried with this:
ALTER TABLE Persons
DROP CONSTRAINT pk_PersonID
ALTER TABLE Persons
ADD PRIMARY KEY (P_Id)
and I get syntax error in Management studio.
This checks if primary key exists, if not it is created
IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS
WHERE CONSTRAINT_TYPE = 'PRIMARY KEY' AND TABLE_NAME = 'Persons'
AND TABLE_SCHEMA ='dbo')
BEGIN
ALTER TABLE Persons ADD CONSTRAINT pk_PersonID PRIMARY KEY (P_Id)
END
ELSE
BEGIN
-- Key exists
END
fiddle: http://sqlfiddle.com/#!6/e165d/2
ALTER TABLE Persons
ADD CONSTRAINT pk_PersonID PRIMARY KEY (P_Id)
An IDENTITY constraint can't be added to an existing column, so how you add this needs to be your initial thought. There are two options:
Create a new table including a primary key with identity and drop the existing table
Create a new primary key column with identity and drop the existing 'P_ID' column
There is a third way, which is a better approach for very large tables via the ALTER TABLE...SWITCH statement. See Adding an IDENTITY to an existing column for an example of each. In answer to this question, if the table isn't too large, I recommend running the following:
-- Check that the table/column exist and no primary key is already on the table.
IF COL_LENGTH('PERSONS','P_ID') IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS
WHERE CONSTRAINT_TYPE = 'PRIMARY KEY' AND TABLE_NAME = 'PERSONS')
-- Add table schema to the WHERE clause above e.g. AND TABLE_SCHEMA ='dbo'
BEGIN
ALTER TABLE PERSONS
ADD P_ID_new int IDENTITY(1, 1)
GO
ALTER TABLE PERSONS
DROP COLUMN P_ID
GO
EXEC sp_rename 'PERSONS.P_ID_new', 'P_ID', 'Column'
GO
ALTER TABLE PERSONS
ADD CONSTRAINT PK_P_ID PRIMARY KEY CLUSTERED (P_ID)
GO
END
Notes:
By explicitly using the CONSTRAINT keyword the primary key constraint is given a particular name rather than depending on SQL Server to auto-assign a name.
Only include CLUSTERED on the PRIMARY KEY if the balance of searches for a particular P_ID and the amount of writing outweighs the benefits of clustering the table by some other index. See Create SQL IDENTITY as PRIMARY KEY.
You can check if primary key exists or not using OBJECTPROPERTY Transact SQL, use 'TableHasPrimaryKey' for the second arguments.
DECLARE #ISHASPRIMARYKEY INT;
SELECT #ISHASPRIMARYKEY = OBJECTPROPERTY(OBJECT_ID('PERSONS'), 'TABLEHASPRIMARYKEY');
IF #ISHASPRIMARYKEY IS NULL
BEGIN
-- generate identity column
ALTER TABLE PERSONS
DROP COLUMN P_ID;
ALTER TABLE PERSONS
ADD P_ID INT IDENTITY(1,1);
-- add primary key
ALTER TABLE PERSONS
ADD CONSTRAINT PK_PERSONID PRIMARY KEY (P_ID);
END;
I don't think you can do that. For making a column into an identity column I think you have to drop the table entirely.

Foreign keys in postgresql can be violated by trigger

I've created some tables in postgres, added a foreign key from one table to another and set ON DELETE to CASCADE. Strangely enough, I have some fields that appear to be violating this constraint.
Is this normal behaviour? And if so, is there a way to get the behaviour I want (no violations possible)?
Edit:
I orginaly created the foreign key as part of CREATE TABLE, just using
... REFERENCES product (id) ON UPDATE CASCADE ON DELETE CASCADE
The current code pgAdmin3 gives is
ALTER TABLE cultivar
ADD CONSTRAINT cultivar_id_fkey FOREIGN KEY (id)
REFERENCES product (id) MATCH SIMPLE
ON UPDATE CASCADE ON DELETE CASCADE;
Edit 2:
To Clarify, I have a sneaking suspicion that the constraints are only checked when updates/inserts happen but are then never looked at again. Unfortunately I don't know enough about postgres to find out if this is true or how fields could end up in the database without those checks being run.
If this is the case, is there some way to check all the foreign keys and fix those problems?
Edit 3:
A constraint violation can be caused by a faulty trigger, see below
I tried to create a simple example that shows foreign key constraint being enforced. With this example I prove I'm not allowed to enter data that violates the fk and I prove that if the fk is not in place during insert, and I enable the fk, the fk constraint throws an error telling me data violates the fk. So I'm not seeing how you have data in the table that violates a fk that is in place. I'm on 9.0, but this should not be different on 8.3. If you can show a working example that proves your issue that might help.
--CREATE TABLES--
CREATE TABLE parent
(
parent_id integer NOT NULL,
first_name character varying(50) NOT NULL,
CONSTRAINT pk_parent PRIMARY KEY (parent_id)
)
WITH (
OIDS=FALSE
);
ALTER TABLE parent OWNER TO postgres;
CREATE TABLE child
(
child_id integer NOT NULL,
parent_id integer NOT NULL,
first_name character varying(50) NOT NULL,
CONSTRAINT pk_child PRIMARY KEY (child_id),
CONSTRAINT fk1_child FOREIGN KEY (parent_id)
REFERENCES parent (parent_id) MATCH SIMPLE
ON UPDATE CASCADE ON DELETE CASCADE
)
WITH (
OIDS=FALSE
);
ALTER TABLE child OWNER TO postgres;
--CREATE TABLES--
--INSERT TEST DATA--
INSERT INTO parent(parent_id,first_name)
SELECT 1,'Daddy'
UNION
SELECT 2,'Mommy';
INSERT INTO child(child_id,parent_id,first_name)
SELECT 1,1,'Billy'
UNION
SELECT 2,1,'Jenny'
UNION
SELECT 3,1,'Kimmy'
UNION
SELECT 4,2,'Billy'
UNION
SELECT 5,2,'Jenny'
UNION
SELECT 6,2,'Kimmy';
--INSERT TEST DATA--
--SHOW THE DATA WE HAVE--
select parent.first_name,
child.first_name
from parent
inner join child
on child.parent_id = parent.parent_id
order by parent.first_name, child.first_name asc;
--SHOW THE DATA WE HAVE--
--DELETE PARENT WHO HAS CHILDREN--
BEGIN TRANSACTION;
delete from parent
where parent_id = 1;
--Check to see if any children that were linked to Daddy are still there?
--None there so the cascade delete worked.
select parent.first_name,
child.first_name
from parent
right outer join child
on child.parent_id = parent.parent_id
order by parent.first_name, child.first_name asc;
ROLLBACK TRANSACTION;
--TRY ALLOW NO REFERENTIAL DATA IN--
BEGIN TRANSACTION;
--Get rid of fk constraint so we can insert red headed step child
ALTER TABLE child DROP CONSTRAINT fk1_child;
INSERT INTO child(child_id,parent_id,first_name)
SELECT 7,99999,'Red Headed Step Child';
select parent.first_name,
child.first_name
from parent
right outer join child
on child.parent_id = parent.parent_id
order by parent.first_name, child.first_name asc;
--Will throw FK check violation because parent 99999 doesn't exist in parent table
ALTER TABLE child
ADD CONSTRAINT fk1_child FOREIGN KEY (parent_id)
REFERENCES parent (parent_id) MATCH SIMPLE
ON UPDATE CASCADE ON DELETE CASCADE;
ROLLBACK TRANSACTION;
--TRY ALLOW NO REFERENTIAL DATA IN--
--DROP TABLE parent;
--DROP TABLE child;
Everything I've read so far seems to suggest that constraints are only checked when the data is inserted. (Or when the constraint is created) For example the manual on set constraints.
This makes sense and - if the database works properly - should be good enough. I'm still curious how I managed to circumvent this or if I just read the situation wrong and there was never a real constraint violation to begin with.
Either way, case closed :-/
------- UPDATE --------
There was definitely a constraint violation, caused by a faulty trigger. Here's a script to replicate:
-- Create master table
CREATE TABLE product
(
id INT NOT NULL PRIMARY KEY
);
-- Create second table, referencing the first
CREATE TABLE example
(
id int PRIMARY KEY REFERENCES product (id) ON DELETE CASCADE
);
-- Create a (broken) trigger function
--CREATE LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION delete_product()
RETURNS trigger AS
$BODY$
BEGIN
DELETE FROM product WHERE product.id = OLD.id;
-- This is an error!
RETURN null;
END;
$BODY$
LANGUAGE plpgsql;
-- Add it to the second table
CREATE TRIGGER example_delete
BEFORE DELETE
ON example
FOR EACH ROW
EXECUTE PROCEDURE delete_product();
-- Now lets add a row
INSERT INTO product (id) VALUES (1);
INSERT INTO example (id) VALUES (1);
-- And now lets delete the row
DELETE FROM example WHERE id = 1;
/*
Now if everything is working, this should return two columns:
(pid,eid)=(1,1). However, it returns only the example id, so
(pid,eid)=(0,1). This means the foreign key constraint on the
example table is violated.
*/
SELECT product.id AS pid, example.id AS eid FROM product FULL JOIN example ON product.id = example.id;