Received: from localhost (maia-1.hub.org [200.46.204.191]) by postgresql.org (Postfix) with ESMTP id C75E79FA435 for ; Fri, 26 Jan 2007 06:16:15 -0400 (AST) Received: from postgresql.org ([200.46.204.71]) by localhost (mx1.hub.org [200.46.204.191]) (amavisd-new, port 10024) with ESMTP id 49877-09 for ; Fri, 26 Jan 2007 06:16:09 -0400 (AST) X-Greylist: delayed 00:33:19.362801 by SQLgrey-1.7.4 Received: from svr4.postgresql.org (svr4.postgresql.org [66.98.251.159]) by postgresql.org (Postfix) with ESMTP id 6C4BA9FB1D5 for ; Fri, 26 Jan 2007 06:16:09 -0400 (AST) X-Greylist: domain auto-whitelisted by SQLgrey-1.7.3 Received: from mx.oldany.cz (donjuan.oldany.cz [82.208.44.152]) by svr4.postgresql.org (Postfix) with ESMTP id C467B5B005B for ; Fri, 26 Jan 2007 09:42:48 +0000 (GMT) Received: from localhost (localhost.localdomain [127.0.0.1]) by mx.oldany.cz (Postfix) with ESMTP id 399E2678046 for ; Fri, 26 Jan 2007 10:42:16 +0100 (CET) X-Virus-Scanned: amavisd-new at oldany.cz Received: from mx.oldany.cz ([127.0.0.1]) by localhost (donjuan.oldany.cz [127.0.0.1]) (amavisd-new, port 10024) with ESMTP id p80Mi+uknGd2 for ; Fri, 26 Jan 2007 10:42:08 +0100 (CET) Received: from [192.168.1.45] (133.134.broadband7.iol.cz [88.102.134.133]) by mx.oldany.cz (Postfix) with ESMTP id 861997780D3 for ; Fri, 26 Jan 2007 10:41:28 +0100 (CET) Message-ID: <45B9CCC6.4000109@fuzzy.cz> Date: Fri, 26 Jan 2007 10:41:26 +0100 From: Tomas Vondra User-Agent: Thunderbird 1.5.0.5 (X11/20060719) MIME-Version: 1.0 To: pgsql-sql@postgresql.org Subject: implementing (something like) UNIQUE constraint using PL/pgSQL Content-Type: text/plain; charset=ISO-8859-1; format=flowed Content-Transfer-Encoding: 7bit X-Virus-Scanned: Maia Mailguard 1.0.1 X-Spam-Status: No, hits=0.745 tagged_above=0 required=5 tests=AWL, BAYES_40, FORGED_RCVD_HELO, RCVD_IN_BL_SPAMCOP_NET, UPPERCASE_25_50 X-Spam-Level: X-Archive-Number: 200701/133 X-Sequence-Number: 27285 Hello, in our application we need to implement a constraint that enforces 'at most N rows with this value', that is we have a table with 'flag' column and for each value there should be at most 10 rows (for example, the exact number does not matter). I'm trying to implement a PL/pgSQL trigger to enforce this constraint, but I'm not sure my solution is 'the best one possible'. The first solution I came up with is this: ===================================================================== CREATE OR REPLACE FUNCTION at_most() RETURNS trigger AS $$ DECLARE v_cnt INTEGER; p_cnt INTEGER; BEGIN IF TG_NARGS >= 1 THEN p_cnt := TG_ARGV[0]::integer; ELSE p_cnt := 1; END IF; SELECT COUNT(*) INTO v_cnt FROM my_table WHERE flag = NEW.flag; IF v_cnt > p_cnt THEN RAISE EXCEPTION 'Too many rows with this flag!' END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER at_most AFTER INSERT OR UPDATE ON my_table FOR EACH ROW EXECUTE PROCEDURE at_most(10); ===================================================================== But that obviously does not work as two sessions can reach the SELECT INTO statement at the same time (or until one of them commits). Thus there could be more than 'cnt' rows with the same value. Then I used a 'FOR UPDATE' lock on a separate 'lock table' that already contains all possible values of 'flag' (in reality the trigger tries to insert that value and catches the 'duplicate key' exception but that's not important here). The trigger is then ===================================================================== CREATE OR REPLACE FUNCTION at_most() RETURNS trigger AS $$ DECLARE v_cnt INTEGER; p_cnt INTEGER; BEGIN IF TG_NARGS >= 1 THEN p_cnt := TG_ARGV[0]::integer; ELSE p_cnt := 1; END IF; PERFORM flag FROM lock_table WHERE flag = NEW.flag FOR UPDATE; SELECT COUNT(*) INTO v_cnt FROM my_table WHERE flag = NEW.flag; IF v_cnt > p_cnt THEN RAISE EXCEPTION 'Too many rows with this flag!'; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER at_most AFTER INSERT OR UPDATE ON my_table FOR EACH ROW EXECUTE PROCEDURE at_most(10); ===================================================================== This works (af far as I know), but I'm not sure it's the best possible solution - for example I'd like to remove the lock table. Is there some 'more elegant' way to do this? Tomas PS: Is there some up to date 'best practices' book related to PL/pgSQL? All books I've found on Amazon are pretty old (about 5 years) or are related to 'beginners' or different areas of development (PHP, etc.)