July 20th, 2011 by depesz | Tags: , , , , , , , , , | 1 comment »
Did it help? If yes - maybe you can help me?

On 18th of July, Tom Lane committed patch:

Add GET STACKED DIAGNOSTICS plpgsql command to retrieve exception info.
 
This is more SQL-spec-compliant, more easily extensible, and better
performing than the old method of inventing special variables.
 
Pavel Stehule, reviewed by Shigeru Hanada and David Wheeler

The problem that it solves is not very common one, but it is a very nifty feature for those of us that write triggers and functions in PostgreSQL.

Let's imagine a simple situation. Two tables:

     Table "public.test1"
 Column |  Type   | Modifiers
--------+---------+-----------
 id     | integer | not null
Indexes:
    "test1_pkey" PRIMARY KEY, btree (id)
 
     Table "public.test2"
 Column |  Type   | Modifiers
--------+---------+-----------
 id     | integer | not null
Indexes:
    "test2_pkey" PRIMARY KEY, btree (id)

But on test2, let's add simple trigger:

CREATE OR REPLACE FUNCTION test2_trg() RETURNS TRIGGER AS
$BODY$
DECLARE
    _id1 INT4;
BEGIN
    _id1 := NEW.id % 100;
    INSERT INTO test1(id) VALUES (_id1);
    RETURN NEW;
END;
$BODY$
LANGUAGE 'plpgsql';
CREATE TRIGGER test2_trg AFTER INSERT ON test2 FOR EACH ROW EXECUTE PROCEDURE test2_trg();

So, whenever I'll insert row to test2, it will also insert row to test1, but with value of id being in range <0..99>

Let's see how it works:

$ INSERT INTO test2 (id) VALUES (1), (5), (120);
INSERT 0 3
 
$ SELECT * FROM test2;
 id
-----
   1
   5
 120
(3 rows)
 
$ SELECT * FROM test1;
 id
----
  1
  5
 20
(3 rows)

Now, let's imagine, that we want to have errors trapped from the inserts, so we write simple function that does the insert and catches error:

CREATE OR REPLACE FUNCTION test(INT4) RETURNS void as $$
BEGIN
    BEGIN
        INSERT INTO test2 (id) VALUES ($1);
    EXCEPTION WHEN others THEN
        raise notice 'Got exception: state: %, message: %', SQLSTATE, SQLERRM;
    END;
    RETURN;
END;
$$ language plpgsql;

Another sanity check:

$ SELECT test( 10 );
 test
------
 
(1 row)
 
$ SELECT * FROM test1;
 id
----
  1
  5
 20
 10
(4 rows)
 
$ SELECT * FROM test2;
 id
-----
   1
   5
 120
  10
(4 rows)

It's all good. So, what will happen if I'll try to insert row that shouldn't be possible? There are two cases:

  • unique violation in test2 table (which by definition would also cause unique violation in test1, because of trigger, but violation in test2 should prevent the trigger from firing)
  • unique violation only in test1 – for example by inserting value “20" to test2 – which doesn't exist in test2, but it does in test1

Let's see what happens, in first case:

$ SELECT test( 10 );
psql:test.sql:39: NOTICE:  Got exception: state: 23505, message: duplicate key value violates unique constraint "test2_pkey"
 test
------
 
(1 row)

and in second:

SELECT test( 20 );
psql:test.sql:39: NOTICE:  Got exception: state: 23505, message: duplicate key value violates unique constraint "test1_pkey"
 test
------
 
(1 row)

Please note that the only thing that differs the error informations is the message. And this is only because my trigger did insert to another table. What would happen if it would insert to the same table? Or there could be multiple inserts to given table, and then the errors are indistinguishable.

But now, with this new patch situation is different.

We can change the test(INT4) function to:

CREATE OR REPLACE FUNCTION test(INT4) RETURNS void as $$
DECLARE
    v_state   TEXT;
    v_msg     TEXT;
    v_detail  TEXT;
    v_hint    TEXT;
    v_context TEXT;
BEGIN
    BEGIN
        INSERT INTO test2 (id) VALUES ($1);
    EXCEPTION WHEN others THEN
        GET STACKED DIAGNOSTICS
            v_state   = RETURNED_SQLSTATE,
            v_msg     = MESSAGE_TEXT,
            v_detail  = PG_EXCEPTION_DETAIL,
            v_hint    = PG_EXCEPTION_HINT,
            v_context = PG_EXCEPTION_CONTEXT;
        raise notice E'Got exception:
            state  : %
            message: %
            detail : %
            hint   : %
            context: %', v_state, v_msg, v_detail, v_hint, v_context;
    END;
    RETURN;
END;
$$ language plpgsql;

The notices will now look now:

SELECT test( 10 );
psql:test.sql:54: NOTICE:  Got exception:
            state  : 23505
            message: duplicate key value violates unique constraint "test2_pkey"
            detail : Key (id)=(10) already exists.
            hint   :
            context: SQL statement "INSERT INTO test2 (id) VALUES ($1)"
PL/pgSQL function "test" line 10 at SQL statement
 test
------
 
(1 row)

and:

SELECT test( 20 );
psql:test.sql:54: NOTICE:  Got exception:
            state  : 23505
            message: duplicate key value violates unique constraint "test1_pkey"
            detail : Key (id)=(20) already exists.
            hint   :
            context: SQL statement "INSERT INTO test1(id) VALUES (_id1)"
PL/pgSQL function "test2_trg" line 6 at SQL statement
SQL statement "INSERT INTO test2 (id) VALUES ($1)"
PL/pgSQL function "test" line 10 at SQL statement
 test
------
 
(1 row)

Which means we now have full access to details, hints, and (what's most important) full context (a.k.a. stack trace).

Of course you don't have to get all diagnostics – you can choose which ones are important, and why.

This, plus added in 8.4 ability to raise exceptions with defined hint/detail fields, means that we can now easily distinguish between errors happening in different places. Great stuff.

  1. One comment

  2. # Keith
    Jul 20, 2011

    Very nice! Thanks for the demonstration

Sorry, comments for this post are disabled.