skip to Main Content

My end user is going to enter query like this:

INSERT INTO course(
    id, column_1, column_2, column_3, column_4, column_5)
    VALUES ('f34-gr5-t46','ABC', '2022-10-18 07:19:29', 2, 'false', now())
    ON CONFLICT (id) DO UPDATE
SET (column_1, column_2, column_3, column_4, column_5)
    = (EXCLUDED.column_1, EXCLUDED.column_2, EXCLUDED.column_3, EXCLUDED.column_4, EXCLUDED.column_5);

User is going to either create new record or update using this same query. Whenever there is an update I need to store only updated column’s name, old value, new value and changed time in my audit table.

I have created a function and a trigger like this:

CREATE OR REPLACE FUNCTION log_changes()
  RETURNS TRIGGER 
  LANGUAGE PLPGSQL
  AS
$$
BEGIN
IF NEW.column_1 != OLD.column_1 THEN
INSERT INTO course_history(id, course_id, field_name, action_type, old_value, new_value,  changed_time)
VALUES(uuid_generate_v4(), OLD.id, 'column_1', 'UPDATE', OLD.column_1, NEW.column_1, new.changed_time);

ELSEIF NEW.column_2 != OLD.column_2 THEN
INSERT INTO course_history(id, course_id, field_name, action_type, old_value, new_value,  changed_time)
VALUES(uuid_generate_v4(), OLD.id, 'column_2', 'UPDATE', OLD.column_2, NEW.column_2, new.changed_time);

// and so on for all columns that could be changed in course table

END IF;
RETURN NEW;
END;
$$

CREATE TRIGGER course_changes
  BEFORE UPDATE
  ON course
  FOR EACH ROW
  EXECUTE PROCEDURE log_changes();

This function creates new rows for each updated fields in single update. I am trying to reduce repeating code of if condition for each column. How can I achieve that?

Is there a way to combine updates of multiple fields into single row? so that if user updated only two columns in single query, I will have single row showing those updates in history table.

2

Answers


  1. To answer your second question : you can insert only one row with updates on several columns, by using a jsonb column in your table course_history in place of fields field_name, action_type, old_value, new_value, and put only one INSERT statement in your trigger function :

    INSERT INTO course_history(id, course_id, changes_jsonb, changed_time)
    SELECT uuid_generate_v4(), OLD.id, jsonb_agg(l.change_jsonb) FILTER (WHERE l.change_jsonb IS NOT NULL), new.changed_time)
      FROM (SELECT CASE WHEN OLD.column_1 != NEW.column_1 THEN jsonb_build_object('field_name', 'column_1', 'action_type', 'UPDATE', 'old_value', OLD.column_1, 'new_value', NEW.column_1) ELSE NULL END AS change_jsonb
           UNION ALL
           SELECT CASE WHEN OLD.column_2 != NEW.column_2 THEN jsonb_build_object('field_name', 'column_2', 'action_type', 'UPDATE', 'old_value', OLD.column_2, 'new_value', NEW.column_2) ELSE NULL END
           UNION ALL
           SELECT CASE WHEN OLD.column_3 != NEW.column_3 THEN jsonb_build_object('field_name', 'column_3', 'action_type', 'UPDATE', 'old_value', OLD.column_3, 'new_value', NEW.column_3) ELSE NULL END
           UNION ALL ... ) AS l
    

    To answer your first question : you can try using a dynamic sql command in you trigger function :

    DECLARE
      col text ;
      txt text ;
    BEGIN
      FOR EACH col IN ARRAY array['column_1', 'column_2', 'column_3', ...]
      LOOP
        IF txt <> '' 
        THEN txt = txt || ' UNION ALL ' ;
        END IF ;
        txt = txt || 'SELECT CASE WHEN OLD.' || col || ' != NEW.' || col || E' THEN jsonb_build_object('field_name', ' || col || E', 'action_type', 'UPDATE', 'old_value', OLD.' || col || E', 'new_value', NEW.' || col || ') ELSE NULL END AS change_jsonb'
      END LOOP ;
      EXECUTE 'INSERT INTO course_history(id, course_id, changes_jsonb, changed_time)
        SELECT uuid_generate_v4(), OLD.id, jsonb_agg(l.change_jsonb) FILTER (WHERE l.change_jsonb IS NOT NULL), new.changed_time) FROM (' || txt || ') AS l' ;
      RETURN NEW ;
    END ;
    
    Login or Signup to reply.
  2. You can get the column names from information_schema.columns. To compare the values for a column, you can use code like:

    DECLARE
       is_distinct boolean;
       col_name text;
    BEGIN
       /* get col_name FROM the metadata */
       EXECUTE format('SELECT $1.%1I IS DISTINCT FROM $2.%1I', col_name)
          INTO is_distinct USING OLD, NEW;
       IF is_distinct THEN
          /* whatever */
       END IF;
    END;
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search