Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Deferrable check constraint in PostgreSQL

I have function checking mandatory participation as follows:

CREATE FUNCTION member_in_has_address()
RETURNS BOOLEAN AS $$
BEGIN
RETURN EXISTS (SELECT *
       FROM address a, member_details b
       WHERE b.member_id = a.member_id);
END;
$$  LANGUAGE plpgsql;

Then called from CHECK constraint

ALTER TABLE member_details
 ADD CONSTRAINT member_in_has_address_check
  CHECK (member_in_has_address());

To create deferable constraint in Standard SQL it would be:

ALTER TABLE member_details
 ADD CONSTRAINT member_in_has_address_check
  INITIALLY DEFERRED
  CHECK (member_in_has_address()); 

How can I do the same in PostgreSQL?

like image 783
Radovan Luptak Avatar asked May 01 '13 17:05

Radovan Luptak


People also ask

Can defer constraint checks?

DEFERRED constraints are not checked until transaction commit. Each constraint has its own IMMEDIATE or DEFERRED mode. Upon creation, a constraint is given one of three characteristics: DEFERRABLE INITIALLY DEFERRED , DEFERRABLE INITIALLY IMMEDIATE , or NOT DEFERRABLE .

How do I find unique constraints in PostgreSQL?

SELECT conname FROM pg_constraint WHERE conrelid = (SELECT oid FROM pg_class WHERE relname LIKE 'tableName'); Also you can get it from pgAdmin in objects tree.

What is a deferred constraint?

A deferred constraint is one that is enforced when a transaction is committed. A deferrable constraint is specified by using DEFERRABLE clause. Once you've added a constraint, you cannot change it to DEFERRABLE. You must drop and recreate the constraint.


3 Answers

You can defer constraints in PostgreSQL in the same way as in other RDBMSs, but for current version (9.2) you can only defer UNIQUE, PRIMARY KEY, EXCLUDE, and REFERENCES. Extract from this page of the manual:

DEFERRABLE
NOT DEFERRABLE

This controls whether the constraint can be deferred. A constraint that is not deferrable will be checked immediately after every command. Checking of constraints that are deferrable can be postponed until the end of the transaction (using the SET CONSTRAINTS command). NOT DEFERRABLE is the default. Currently, only UNIQUE, PRIMARY KEY, EXCLUDE, and REFERENCES (foreign key) constraints accept this clause. NOT NULL and CHECK constraints are not deferrable.

INITIALLY IMMEDIATE
INITIALLY DEFERRED

If a constraint is deferrable, this clause specifies the default time to check the constraint. If the constraint is INITIALLY IMMEDIATE, it is checked after each statement. This is the default. If the constraint is INITIALLY DEFERRED, it is checked only at the end of the transaction. The constraint check time can be altered with the SET CONSTRAINTS command.

You can create a simple deferred foreign key from member_details to address instead of your current constraint to check, if every member has an address.

UPDATE: You need to create 2 foreign keys. One regular one from address(member_id) to member_details(member_id). The other one - deferred from member_details(member_id) to address(member_id).

With this two foreign keys you will be able to:

  1. Create a member in member_details.
  2. Create an address in address for member from step 1
  3. Commit (with no errors)

OR

  1. Create a member in member_details.
  2. Commit (and get error from deferred foreign key).
like image 159
Ihor Romanchenko Avatar answered Oct 13 '22 16:10

Ihor Romanchenko


Wrap your queries in a transaction, and then use a deferred foreign key and deferred constraint triggers if at least one address is needed:

CREATE CONSTRAINT TRIGGER member_details_address_check_ins
  AFTER INSERT ON member_details
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW
EXECUTE PROCEDURE member_details_address_check_ins();

ALTER TABLE address
ADD CONSTRAINT address_member_details_member_id_fkey
FOREIGN KEY (member_id) REFERENCES member_details(member_id)
ON UPDATE NO ACTION ON DELETE NO ACTION
DEFERRABLE INITIALLY DEFERRED;

CREATE CONSTRAINT TRIGGER address_member_details_check_del
  AFTER DELETE ON address
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW
EXECUTE PROCEDURE address_member_details_check_del();

-- also consider the update cases for the inevitable merge of duplicate members.

On a separate note, normalized and pretty, but putting addresses and contact details such as emails in a separate address table occasionally introduces very colorful UI/UX issues. E.g. an untrained secretary changing the company and address of all of her boss' contacts at company A when one of them switched to company B. Yeah, seen it happen for real when the UI behaved differently from Outlook...

Anyway, and fwiw, I've found that it's usually more convenient to store this stuff in the same table as the contact, i.e. address1, address2, email1, email2, etc. It makes other things simpler for a variety of other reasons -- namely running checks like the one you're looking into. The extremely rare case where you'd want to store more than two such pieces of information are, in practice, simply not worth the hassle.

like image 43
Denis de Bernardy Avatar answered Oct 13 '22 16:10

Denis de Bernardy


This is what I come up with.

ALTER TABLE address
ADD CONSTRAINT address_member_in_has_address
FOREIGN KEY (member_id) REFERENCES member_details(member_id)
ON DELETE CASCADE
DEFERRABLE INITIALLY DEFERRED;

CREATE FUNCTION member_in_has_address() RETURNS trigger AS $BODY$
    BEGIN
    IF NOT EXISTS(SELECT * 
                   FROM member_details
                   WHERE member_id IN (SELECT member_id 
                                        FROM address)) 
    THEN
            RAISE EXCEPTION 'Error: member does not have address';
        END IF;
    RETURN NEW;
    END;
$BODY$ LANGUAGE plpgsql;

CREATE CONSTRAINT TRIGGER manatory_participation_member_details_ins
 AFTER INSERT ON member_details 
 DEFERRABLE INITIALLY DEFERRED 
 FOR EACH ROW  
 EXECUTE PROCEDURE member_in_has_address();

CREATE CONSTRAINT TRIGGER manatory_participation_member_details_del
 AFTER INSERT ON member_details 
 DEFERRABLE INITIALLY DEFERRED 
 FOR EACH ROW 
 EXECUTE PROCEDURE member_in_has_address();

I tried Igor's version using foreign keys in both tables without the triggers. In this case this constraint is not deffered.

ALTER TABLE member_details
ADD CONSTRAINT member_details_in_has_address
FOREIGN KEY (address_id) REFERENCES address
ON UPDATE NO ACTION ON DELETE CASCADE
DEFERRABLE INITIALLY DEFERRED;

I get this: ERROR: null value in column "address_id" violates not-null constraint

When inserting using this annonymous block:

DO $$ 
DECLARE 
 mem BIGINT;
BEGIN
INSERT INTO member_details (member_first_name, member_last_name, member_dob, member_phone_no, 
member_email, member_gender, industry_position, account_type, music_interests)
VALUES ('Rado','Luptak','07/09/80','07540962233','[email protected]','M','DJ','basic','hard core');

SELECT member_id 
 INTO mem
FROM member_details
WHERE member_first_name = 'Rado' AND member_last_name = 'Luptak'
AND member_dob = '07/09/76';

INSERT INTO address (address_id, house_name_no, post_code, street_name, town, country, member_id)
VALUES (mem, '243', 'E17 3TT','Wood Road','London', 'UK', mem);

UPDATE member_details
 SET  address_id = mem WHERE member_id = mem;
END
$$;

Another problem with enforcing mandatory participation in member_details using address_id of address table (Igor's version) is that this allows me to insert row into member_details and reference an existing address row, but the existing address row references different member_details row. When the latter member_details row is deleted it cascades and deletes the address row, which can or cannot delete (depends on settings) the new inserted member_details row. It would also return different details when joining on member_id and on address_id. Therefore, it requires another constraint, so I stayed with trigger and dropping it before insert and recreating it after insert, due to the trigger is not deferred.

like image 2
Radovan Luptak Avatar answered Oct 13 '22 16:10

Radovan Luptak