Search code examples
postgresqltriggersuser-defined-functionsplpgsql

Dynamically Access Columns and Cast Data Types in PostgreSQL Functions


In custom functions in PostgreSQL, I can access the NEW object to retrieve the values of specific columns, for example:

NEW.description 
NEW.city

Essentially, this is a static reference at the code-writing stage. But is it possible to achieve the same dynamically, for example, similar to how it’s done in Map structures:

NEW['description'] 
NEW['name']

In other words, at the code-writing stage, I don’t know which specific column in NEW I will be referencing, as this is determined dynamically during the function’s execution.

And the same question applies to type casting—can I dynamically cast the retrieved value to a type that is also determined during the function’s execution? For example, INTEGER, BOOLEAN, etc. That is, I dynamically obtain these types as TEXT during the function's execution, perhaps from a query to another configuration table, and I would like to cast the column value from NEW to such a type.


Solution

  • NEW is just a record that you can automagically convert to a jsonb with to_jsonb(NEW) and get the exact syntax and behaviour you want:
    demo at db<>fiddle

    create table test(
       a int generated by default as identity primary key
      ,b boolean,c text,d "char",e "char",f "char");
    
    create table debug_(
       payload jsonb
      ,comment text
      ,is_valid_as_date boolean 
      ,id int generated by default as identity primary key
      ,ts timestamp default clock_timestamp());
    select setseed(.46);
    
    create function f_test_trigger()returns trigger as $f$
    declare new_jsonb jsonb:=to_jsonb(new);
    begin
      insert into debug_ values
       (new_jsonb['c'], 'just one of the columns')
      ,(new_jsonb['x'], 'this column does not exist')
      ,(new_jsonb[chr(97+(random()*25)::int)], '1st random lowercase letter key')
      ,(new_jsonb[chr(97+(random()*25)::int)], '2nd random lowercase letter key');
      update debug_ 
        set is_valid_as_date = pg_input_is_valid( payload#>>'{}'--as unquoted text
                                                 ,'date');
      return new;
    end $f$ language plpgsql;
    
    create trigger test_trigger after insert on test
    for each row execute function f_test_trigger();
    
    insert into test(b,c,d,e,f)values(true,'2024-10-01','d','e','f');
    
    table debug_;
    
    payload comment is_valid_as_date id ts
    "2024-10-01" just one of the columns TRUE 1 2025-01-15 18:26:13.079506
    null this column does not exist null 2 2025-01-15 18:26:13.079812
    null 1st random lowercase letter key null 3 2025-01-15 18:26:13.079839
    "d" 2nd random lowercase letter key false 4 2025-01-15 18:26:13.079847

    As long as the text value you're dealing with is acceptable as an input for your given target type, your cast() will work. Recycling an answer from a related thread: in PostgreSQL 16 and above, there's pg_input_is_valid() that lets you make sure before you attempt to cast().

    pg_input_is_valid ( string text, type text ) → boolean
    Tests whether the given string is valid input for the specified data type, returning true or false. This function will only work as desired if the data type's input function has been updated to report invalid input as a “soft” error. Otherwise, invalid input will abort the transaction, just as if the string had been cast to the type directly.

    pg_input_is_valid('42', 'integer') → t
    pg_input_is_valid('42000000000', 'integer') → f
    pg_input_is_valid('1234.567', 'numeric(7,4)') → f
    

    In PostgreSQL 15 and earlier, you can build your own:

    create or replace function is_interpretable_as(arg text, arg_type text) 
    returns boolean language plpgsql as $$
    begin
        execute format('select cast(%L as %s)', arg, arg_type);
        return true;
    exception when others then
        return false;
    end $$;