15

I would like to record the id of a user in the session/transaction, using SET, so I could be able to access it later in a trigger function, using current_setting. Basically, I'm trying option n2 from a very similar ticket posted previously, with the difference that I'm using PG 10.1 .

I've been trying 3 approaches to setting the variable:

  • SET local myvars.user_id = 4, thereby setting it locally in the transaction;
  • SET myvars.user_id = 4, thereby setting it in the session;
  • SELECT set_config('myvars.user_id', '4', false), which depending of the last argument, will be a shortcut for the previous 2 options.

None of them is usable in the trigger, which receives NULL when getting the variable through current_setting. Here is a script I've devised to troubleshoot it (can be easily used with the postgres docker image):

database=$POSTGRES_DB
user=$POSTGRES_USER
[ -z "$user" ] && user="postgres"

psql -v ON_ERROR_STOP=1 --username "$user" $database <<-EOSQL
    DROP TRIGGER IF EXISTS add_transition1 ON houses;
    CREATE TABLE IF NOT EXISTS houses (
        id SERIAL NOT NULL,
        name VARCHAR(80),
        created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now(),
        PRIMARY KEY(id)
    );

    CREATE TABLE IF NOT EXISTS transitions1 (
        id SERIAL NOT NULL,
        house_id INTEGER,
        user_id INTEGER,
        created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now(),
        PRIMARY KEY(id),
        FOREIGN KEY(house_id) REFERENCES houses (id) ON DELETE CASCADE

    );

    CREATE OR REPLACE FUNCTION add_transition1() RETURNS TRIGGER AS \$\$
        DECLARE
            user_id integer;
        BEGIN
            user_id := current_setting('myvars.user_id')::integer || NULL;
            INSERT INTO transitions1 (user_id, house_id) VALUES (user_id, NEW.id);
            RETURN NULL;
        END;
    \$\$ LANGUAGE plpgsql;

    CREATE TRIGGER add_transition1 AFTER INSERT OR UPDATE ON houses FOR EACH ROW EXECUTE PROCEDURE add_transition1();

    BEGIN;
    %1% SELECT current_setting('myvars.user_id');
    %2% SELECT set_config('myvars.user_id', '55', false);
    %3% SELECT current_setting('myvars.user_id');
    INSERT INTO houses (name) VALUES ('HOUSE PARTY') RETURNING houses.id;
    SELECT * from houses;
    SELECT * from transitions1;
    COMMIT;
    DROP TRIGGER IF EXISTS add_transition1 ON houses;
    DROP FUNCTION IF EXISTS add_transition1;
    DROP TABLE transitions1;
        DROP TABLE houses;
EOSQL

The conclusion I came to was that the function is triggered in a different transaction and a different (?) session. Is this something that one can configure, so that all happens within the same context?

klin
  • 112,967
  • 15
  • 204
  • 232
ChuckE
  • 5,610
  • 4
  • 31
  • 59

3 Answers3

9

Handle all possible cases for the customized option properly:

  1. option not set yet

All references to it raise an exception, including current_setting() unless called with the second parameter missing_ok. The manual:

If there is no setting named setting_name, current_setting throws an error unless missing_ok is supplied and is true.

  1. option set to a valid integer literal

  2. option set to an invalid integer literal

  3. option reset (which burns down to a special case of 3.)

For instance, if you set a customized option with SET LOCAL or set_config('myvars.user_id3', '55', true), the option value is reset at the end of the transaction. It still exists, can be referenced, but it returns an empty string now ('') - which cannot be cast to integer.

Obvious mistakes in your demo aside, you need to prepare for all 4 cases. So:

CREATE OR REPLACE FUNCTION add_transition1()
  RETURNS trigger
  LANGUAGE plpgsql AS
$func$
DECLARE
   _user_id text := current_setting('myvars.user_id', true);  -- see 1.
BEGIN
   IF _user_id ~ '^\d+$' THEN  -- one or more digits?
      INSERT INTO transitions1 (user_id, house_id)
      VALUES (_user_id::int, NEW.id);  -- valid int, cast is safe
   ELSE
      INSERT INTO transitions1 (user_id, house_id)
      VALUES (null, NEW.id);           -- use null instead

      RAISE WARNING 'Invalid user_id % for house_id % was reset to null!'
                  , quote_literal(_user_id), NEW.id;  -- optional
   END IF;

   RETURN NULL;  -- OK for AFTER trigger
END
$func$;

db<>fiddle here

Notes:

  • Avoid variable names matching column names. Very error prone. One popular naming convention is to prepend variable names with an underscore: _user_id.

  • Assign at declaration time to save another assignment. Note the data type text. We'll cast later, after sorting out invalid input.

  • Avoid raising / trapping an exception if possible. The manual:

    A block containing an EXCEPTION clause is significantly more expensive to enter and exit than a block without one. Therefore, don't use EXCEPTION without need.

  • Test for valid integer strings. This simple regular expression allows only digits (no leading sign, no white space): _user_id ~ '^\d+$'. I reset to null for any invalid input. Adapt to your needs.

  • I added an optional WARNING for your debugging convenience.

  • Cases 3. and 4. only arise because customized options are string literals (type text), valid data types cannot be enforced automatically.

Related:

All that aside, there may be more elegant solutions for what you are trying to do without customized options, depending on your exact requirements. Maybe this:

Erwin Brandstetter
  • 605,456
  • 145
  • 1,078
  • 1,228
  • Your response was the most comprehensive of all, although I went with a variation of @garysieling 's response. But you touched on most of the important matters; in the beginning, `myvars.user_id` is NULL, after using `SET LOCAL` is it reset to `''` at transaction's end, and I have to account for all cases. – ChuckE Aug 28 '18 at 11:58
7

It is not clear why you are trying to concat NULL to user_id but it is obviously the cause of the problem. Get rid of it:

CREATE OR REPLACE FUNCTION add_transition1() RETURNS TRIGGER AS $$
    DECLARE
        user_id integer;
    BEGIN
        user_id := current_setting('myvars.user_id')::integer;
        INSERT INTO transitions1 (user_id, house_id) VALUES (user_id, NEW.id);
        RETURN NULL;
    END;
$$ LANGUAGE plpgsql;

Note that

SELECT 55 || NULL

always gives NULL.

klin
  • 112,967
  • 15
  • 204
  • 232
  • but how do I set up the equivalent? I'd like to get NULL back when the setting's not available. – ChuckE Aug 27 '18 at 13:59
  • @ChuckE `user_id := current_setting('myvars.user_id', true)::integer;` – Abelisto Aug 28 '18 at 00:27
  • 2
    @ChuckE PS: I have feeling that you are confusing SQL string concatenation operator `||` and C logical "or" operator `||`... – Abelisto Aug 28 '18 at 00:33
5

You can catch the exception when the value doesn't exist - here's the changes I made to get this to work:

CREATE OR REPLACE FUNCTION add_transition1() RETURNS TRIGGER AS $$
    DECLARE
        user_id integer;
    BEGIN
        BEGIN
            user_id := current_setting('myvars.user_id')::integer;
        EXCEPTION WHEN OTHERS THEN
            user_id := 0;
        END;

        INSERT INTO transitions1 (user_id, house_id) VALUES (user_id, NEW.id);
        RETURN NULL;
    END;
$$ LANGUAGE plpgsql;

 CREATE OR REPLACE FUNCTION insert_house() RETURNS void as $$
 DECLARE
    user_id integer;
 BEGIN 
   PERFORM set_config('myvars.user_id', '55', false);

   INSERT INTO houses (name) VALUES ('HOUSE PARTY');
 END; $$ LANGUAGE plpgsql;
garysieling
  • 346
  • 2
  • 7
  • Although I haven't selected it, your example provided the blueprint for my final implementation. Thx a bunch! – ChuckE Aug 28 '18 at 11:58