Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use variable settings in trigger functions?

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?

like image 330
ChuckE Avatar asked Aug 16 '18 15:08

ChuckE


People also ask

Which variable tells for which operation the trigger was fire?

TG_OP. Data type text ; a string of INSERT , UPDATE , DELETE , or TRUNCATE telling for which operation the trigger was fired.

Which variable contains the number of argument given to the trigger procedure?

TG_NARGS. Data type integer; the number of arguments given to the trigger procedure in the CREATE TRIGGER statement.

What is the purpose of new variable in trigger?

NEW and OLD are special variables that you can use with PL/SQL triggers without explicitly defining them. NEW is a pseudo-record name that refers to the new table row for insert and update operations in row-level triggers. Its usage is :NEW.

What are the variables contained in the system trigger class?

These variables are contained in the System.Trigger class. Returns true if the current context for the Apex code is a trigger, not a Visualforce page, a Web service, or an executeanonymous () API call. Returns true if this trigger was fired due to an insert operation, from the Salesforce user interface, Apex, or the API.

What is context variable in system trigger?

Trigger Context Variables All triggers define implicit variables that allow developers to access run-time context. These variables are contained in the System.Trigger class. The record firing a trigger can include an invalid field value, such as a formula that divides by zero.

What is the default return value of HTTP trigger?

The HTTP trigger lets you invoke a function with an HTTP request. You can use an HTTP trigger to build serverless APIs and respond to webhooks. The default return value for an HTTP-triggered function is: HTTP 204 No Content with an empty body in Functions 2.x and higher. HTTP 200 OK with an empty body in Functions 1.x.

How to have static variables persist across triggers in a class?

Line 4 will always execute, and Line 6 will always set the value to 1. To have static values persist across triggers, one has to put the static variables within a class. Show activity on this post. A static variable retains it`s value throughout an execution context.


1 Answers

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.

  2. option set to a valid integer literal

  3. option set to an invalid integer literal

  4. 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 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$  LANGUAGE plpgsql;

db<>fiddle here

Notes:

  • Avoid variable names that match 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 one 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:

  • User defined variables in PostgreSQL
  • Is there a way to define a named constant in a PostgreSQL query?

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:

  • Fastest way to get current user's OID in Postgres?
like image 163
Erwin Brandstetter Avatar answered Oct 16 '22 07:10

Erwin Brandstetter