Search code examples
sqlpostgresqltriggerssql-delete

How to check if OLD column exist in Postgres Trigger Function


I want to create a deleted logs and insert data from the OLD row column. The problem is the column is not same for each table, some tables only has transaction_date and other table only has created_at. So I want to check if transaction_date just use it, otherwise use created_at column. I tried using coalesce function but still return:

ERROR: record "old" has no field "transaction_date" CONTEXT: SQL statement "INSERT INTO "public"."delete_logs" ("table", "date") VALUES (TG_TABLE_NAME, coalesce(OLD.transaction_date, coalesce(OLD.created_at, now())))" PL/pgSQL function delete_table() line 2 at SQL statement

here is my function:

CREATE OR REPLACE FUNCTION delete_table() RETURNS trigger AS
$$BEGIN
INSERT INTO "public"."deleted_logs" ("table", "created_at") VALUES (TG_TABLE_NAME, coalesce(OLD.transaction_date, coalesce(OLD.created_at, now())));

   RETURN OLD;
END;$$ LANGUAGE plpgsql;


CREATE TRIGGER "testDelete" AFTER DELETE ON "exampletable" FOR EACH ROW EXECUTE PROCEDURE "delete_table"();

Actually, I wanted to create a function for each table, but I think it will be difficult to update the function in the future, so I need to create a single function for all tables.


Solution

  • So I want to check if transaction_date just use it, otherwise use created_at column.

    You can avoid the exception you saw by converting the row to json:

    CREATE OR REPLACE FUNCTION log_ts_after_delete()
      RETURNS trigger
      LANGUAGE plpgsql AS
    $func$
    BEGIN
       INSERT INTO public.deleted_logs
              (table_name   , created_at)  -- "table" is a reserved word
       VALUES (TG_TABLE_NAME, COALESCE(to_json(OLD)->>'transaction_date', to_json(OLD)->>'created_at')::timestamptz);
    
    
       RETURN NULL;  -- not used in AFTER trugger
    END
    $func$;
    

    My answer assumes that transaction_date is defined NOT NULL. Else, the expression defaults to created_at. Probably not what you want.

    JSON is not as strict as SQL. A reference to a non-existing JSON key results in NULL instead of the exception for the reference to a non-existing table column. So COALESCE just works.

    Related:

    If the row is wide, it might be cheaper to convert to JSON only once and save it to a variable, or do it in a subquery or CTE.

    Related:

    If tables never switch the columns in question, passing a parameter in the trigger definition would be much cheaper. You find out (at trigger creation time) once with:

    SELECT attname
    FROM   pg_attribute
    WHERE  attrelid = 'public.exampletable'::regclass
    AND    attname IN ('transaction_date', 'created_at')
    AND    NOT attisdropped 
    ORDER  BY attname DESC
    

    This returns 'transaction_date' if such a column exists in the table, else 'created_at', else NULL (no row). Related:

    It's still cheapest to have a separate trigger function for each type of trigger. Just two functions instead of one. If the trigger is fired often I would do that.

    Avoid exception handling if you can. The manual:

    Tip

    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.