Question

I have a trigger function in PostgreSQL 12 which executes something like this:

CREATE OR REPLACE FUNCTION "my_latlon_function"()
RETURNS trigger AS
$$
BEGIN
    latcolumn:= (
        SELECT column_name
          FROM information_schema.columns
         WHERE table_schema = TG_TABLE_SCHEMA
           AND table_name = TG_TABLE_NAME
           AND column_name ~* '.*lat.*'
    );
    loncolumn := (
        SELECT column_name
          FROM information_schema.columns
         WHERE table_schema = TG_TABLE_SCHEMA
           AND table_name = TG_TABLE_NAME
           AND column_name ~* '.*lon.*'
    );
    EXECUTE 'select $1.' || loncolumn USING NEW INTO lon;
    EXECUTE 'select $1.' || latcolumn USING NEW INTO lat;

    -- do much stuff

    RETURN NEW;

END
$$
LANGUAGE 'plpgsql';

The problem is, the lat and lon columns have capital letters in their name, e.g. myLatitude and myLongitude. The trigger function is able to retrieve these names, that's no problem.

The problem lies in the two EXECUTE statements where it seems that the column names become lower-cased as stated by this error (in the underlying QUERY when the trigger is fired):

ERROR:  column "mylongitude" not found in data type gpspoints
LINE 1: select $1.myLongitude
               ^
QUERY:  select $1.myLongitude
CONTEXT:  PL/pgSQL function my_latlon_function() line 24 at EXECUTE
********** Error **********

ERROR: column "mylongitude" not found in data type gpspoints
SQL state: 42703
Context: PL/pgSQL function my_latlon_function() line 24 at EXECUTE

I know that PostgreSQL does need to enclose column names which have capital letters in their name into double quotes. Hence, I have tried to set double quotes in the two EXECUTE statements such as this:

    EXECUTE 'select $1.' || "loncolumn" USING NEW INTO lon;
    EXECUTE 'select $1.' || "latcolumn" USING NEW INTO lat;

But the error stays exactly the same.

If possible, how can I handle CamelCase column names in a PostgreSQL trigger function?

If not not, why?

Was it helpful?

Solution

Use format with the %I placeholder for identifiers:

EXECUTE format('select $1.%I', latcolumn) USING NEW INTO lat;

OTHER TIPS

Double-quote identifiers automatically where needed with format() (like Laurenz demonstrates), or quote_ident(). See:

Also, your trigger function can be more efficient like this:

CREATE OR REPLACE FUNCTION my_latlon_function()
  RETURNS trigger
  LANGUAGE plpgsql AS
$func$
DECLARE
   _latcol text;
   _loncol text;
   _lat numeric;
   _lon numeric;
BEGIN
   SELECT quote_ident(a.attname) INTO _latcol  -- properly quoted!
   FROM   pg_attribute a
   WHERE  a.attrelid = TG_RELID
   AND    a.attname ~* 'lat'
   AND    a.attnum > 0
   AND    NOT a.attisdropped;

   SELECT quote_ident(a.attname) INTO _loncol
   FROM   pg_attribute a
   WHERE  a.attrelid = TG_RELID
   AND    a.attname ~* 'lon'
   AND    a.attnum > 0
   AND    NOT a.attisdropped;
   
   EXECUTE concat('SELECT $1.', _latcol, ', $1.', _loncol)
   USING  NEW
   INTO   _lat, _lon;

   RAISE NOTICE '% / %', _lat, _lon;  -- debug output

   -- do much stuff

   RETURN NEW;
END
$func$;

db<>fiddle here

(This is assuming column names always match unambiguously!?)

Demonstrating quote_ident(). With column names already quoted properly, we can simply concat() later.

The catalog table pg_catalog.pg_attribute is notably faster than information_schema.columns. Compare the output of these two commands to understand why:

EXPLAIN SELECT * FROM information_schema.columns;
EXPLAIN SELECT * FROM pg_catalog.pg_attribute;

See:

In this particular case, we can also conveniently use TG_RELID instead of TG_TABLE_SCHEMA and TG_TABLE_NAME.

Also, individual assignments are comparatively expensive in PL/pgSQL. Try to keep their number low.

While the difference in performance is still small, this trigger function is called once per inserted row. Adds up for big inserts ...

Aside, col ~* 'lon' is exactly equivalent to col ~* '.*lon.*'.

Licensed under: CC-BY-SA with attribution
Not affiliated with dba.stackexchange
scroll top