Meanings of bits in trigger type field (tgtype) of Postgres pg_trigger

tgtype is a (per documentation):

Bit mask identifying trigger firing conditions

But individual bits are not identified there.
The source code says:

41  int16  tgtype;  /* BEFORE/AFTER/INSTEAD, UPDATE/DELETE/INSERT,
42                                   * ROW/STATEMENT; see below */

...

93  /* Bits within tgtype */
94  #define TRIGGER_TYPE_ROW         (1 << 0)
95  #define TRIGGER_TYPE_BEFORE      (1 << 1)
96  #define TRIGGER_TYPE_INSERT      (1 << 2)
97  #define TRIGGER_TYPE_DELETE      (1 << 3)
98  #define TRIGGER_TYPE_UPDATE      (1 << 4)
99  #define TRIGGER_TYPE_TRUNCATE    (1 << 5)
100 #define TRIGGER_TYPE_INSTEAD     (1 << 6)
101
102 #define TRIGGER_TYPE_LEVEL_MASK  (TRIGGER_TYPE_ROW)
103 #define TRIGGER_TYPE_STATEMENT   0

23 translates to 00010111 in the least significant 8 bits, which should stand for:

CREATE TRIGGER trigger_name
BEFORE INSERT OR UPDATE ON table_name
FOR EACH ROW
EXECUTE PROCEDURE func_name();

A couple of quick tests in existing databases of mine seem to confirm it.


Thanks everyone, these answers saved me some time. Regarding the last answer, it is not overkill, if you have TRUNCATE triggers. I just noticed the other day that such triggers are not included in information_schema.triggers. As I either remember, or speculate..., this is a conscious choice. TRUNCATE is non-standard, and I think the core Postgres folks are strict about not making information_schema non-standard at all.

I did a tiny revision to the posted code for myself, posted here for anyone else who wants such a thing.

SELECT 
  c.relnamespace::regnamespace::text as schema_name, 
  tgrelid::regclass                  as table_name,
  t.tgname                           as trigger_name, 
  proname                            as function_name,
  tgtype::int::bit(7)                as condition_bits,
  CASE WHEN (tgtype::int::bit(7) & b'0000001')::int = 0 THEN 'STATEMENT' ELSE 'EACH ROW' END
                                     as trigger_scope,
  COALESCE(
    CASE WHEN (tgtype::int::bit(7) & b'0000010')::int = 0 THEN NULL ELSE 'BEFORE' END,
    CASE WHEN (tgtype::int::bit(7) & b'0000010')::int = 0 THEN 'AFTER' ELSE NULL END,
    CASE WHEN (tgtype::int::bit(7) & b'1000000')::int = 0 THEN NULL ELSE 'INSTEAD' END,
    ''
  )::text                           as trigger_timing, 
    (CASE WHEN (tgtype::int::bit(7) & b'0000100')::int = 0 THEN '' ELSE ' INSERT' END) ||
    (CASE WHEN (tgtype::int::bit(7) & b'0001000')::int = 0 THEN '' ELSE ' DELETE' END) ||
    (CASE WHEN (tgtype::int::bit(7) & b'0010000')::int = 0 THEN '' ELSE ' UPDATE' END) ||
    (CASE WHEN (tgtype::int::bit(7) & b'0100000')::int = 0 THEN '' ELSE ' TRUNCATE' END)
                                     as trigger_action,
pg_get_triggerdef(t.oid)             as trigger_definition
  
FROM pg_trigger t
JOIN pg_proc p      ON t.tgfoid = p.oid
JOIN pg_class c     ON c.oid    = t.tgrelid

Thanks rlib and Erwin. This was exactly what I was looking for.

I put together a query for this. I'll leave it here if anyone else needs it.

SELECT 
  n.nspname as schema_name, 
  c.relname as table_name, 
  t.tgname as trigger_name, 
  proname as function_name,
  tgtype::int::bit(7) as condition_bits,
  CASE WHEN (tgtype::int::bit(7) & b'0000001')::int = 0 THEN 'STATEMENT' ELSE 'EACH ROW' END as cond_row,
  COALESCE(
    CASE WHEN (tgtype::int::bit(7) & b'0000010')::int = 0 THEN NULL ELSE 'BEFORE' END,
    CASE WHEN (tgtype::int::bit(7) & b'0000010')::int = 0 THEN 'AFTER' ELSE NULL END,
    CASE WHEN (tgtype::int::bit(7) & b'1000000')::int = 0 THEN NULL ELSE 'INSTEAD' END,
    ''
  )::text as cond_timing, 
    (CASE WHEN (tgtype::int::bit(7) & b'0000100')::int = 0 THEN '' ELSE ' INSERT' END) ||
    (CASE WHEN (tgtype::int::bit(7) & b'0001000')::int = 0 THEN '' ELSE ' DELETE' END) ||
    (CASE WHEN (tgtype::int::bit(7) & b'0010000')::int = 0 THEN '' ELSE ' UPDATE' END) ||
    (CASE WHEN (tgtype::int::bit(7) & b'0100000')::int = 0 THEN '' ELSE ' TRUNCATE' END)
  as cond_event
FROM pg_trigger t
JOIN pg_proc p ON t.tgfoid = p.oid
JOIN pg_class c ON c.oid = t.tgrelid
JOIN pg_namespace n ON n.oid = c.relnamespace;

Edit:

Hmm, that was overkill. It turns out that the above bit-work is already done in the triggers view in the information_schema schema.

Easier: SELECT * FROM information_schema.triggers