Return dynamic table with unknown columns from PL/pgSQL function
Asked Answered
U

1

17

I need to create a function that checks on a given table if the infowindow field exists. If it exists the function must return select * from table but if it does not, it must return an additional id field:

CREATE OR REPLACE FUNCTION getxo_ocx_cincu_preparar_infowindow(
                                              guretabla character varying)
  RETURNS TABLE AS
$BODY$ 
DECLARE
    tabla ALIAS FOR $1;

BEGIN

IF  EXISTS (SELECT 1
   FROM   pg_namespace n
   JOIN   pg_class     c ON c.relnamespace = n.oid
   JOIN   pg_attribute a ON a.attrelid = c.oid 
   WHERE  n.nspname = current_schema()  -- default to current schema
   AND    c.relname = tabla
   AND    a.attname = 'infowindow'
   AND    NOT a.attisdropped)
THEN
    RETURN QUERY EXECUTE 'SELECT * from ' ||tabla ;
ELSE
    RETURN QUERY EXECUTE 'SELECT *, ID:' || id::text ||' as infowindow
                                   from ' ||tabla ;
END IF;

END;
$BODY$
  LANGUAGE plpgsql VOLATILE;

If I use RETURNS SETOF RECORDS, when I do the select to the function I need to specify the columns, which I don't know. And if I use RETURNS TABLE I need to specify the fields too, so I don't know how to do it.

Unorthodox answered 29/5, 2014 at 9:11 Comment(1)
You forgot to mention your version of Postgres.Bernini
B
33

This is hard to solve, because SQL demands to know the return type at call time.
Also, a plpgsql function needs to have a well defined return type.

If you choose to return anonymous records, you get what you defined: anonymous records. Postgres does not know what's inside. Therefore, a column definition list is required to decompose the type.

There are various workarounds, depending on exact requirements. If you have any way of knowing the return type at call time, I suggest polymorphic types as outlined in the last chapter of this answer ("Various complete table types"):

But that does not cover adding another column to the return type at runtime inside the function. That's just not possible. I would rethink the whole approach.

As for your current approach, the closest thing I can think of would be a temporary table (or a cursor), that you query in a second call within a single transaction.

You have a couple of other problems in your code. See notes below.

Proof of concept

CREATE OR REPLACE FUNCTION f_tbl_plus_infowindow (_tbl regclass)  -- parameter name
  RETURNS void  -- no direct return type
  LANGUAGE plpgsql AS
$func$
DECLARE
   _tmp text := quote_ident(_tbl::text || '_tmp');  -- apending _tmp for temp table
BEGIN
   -- Create temp table only for duration of transaction
   EXECUTE format(
      'CREATE TEMP TABLE %s ON COMMIT DROP AS
       SELECT * FROM %s LIMIT 0'
      ,_tmp, _tbl
     );

   IF EXISTS (
      SELECT FROM pg_attribute a
      WHERE  a.attrelid = _tbl
      AND    a.attname  = 'infowindow'
      AND    a.attisdropped = FALSE
      )
   THEN
      EXECUTE format('INSERT INTO %s SELECT * FROM %s', _tmp, _tbl);
   ELSE
     -- This is assuming a NOT NULL column named "id"!!
      EXECUTE format($$
         ALTER  TABLE %1$s ADD COLUMN infowindow text;
         INSERT INTO %1$s
         SELECT *, 'ID: ' || id::text
         FROM   %2$s$$
        ,_tmp, _tbl
        );
   END IF;
END
$func$;

The call has to be in a single transaction. You may have to start an explicit transaction, depending on your client.

BEGIN;
SELECT f_tbl_plus_infowindow ('tbl');
SELECT * FROM tbl_tmp;  -- do something with the returned rows
ROLLBACK;               -- or COMMIT, does not matter here

db<>fiddle here
Old sqlfiddle

Alternatively you could let the temporary table live for the duration of the session. Be wary of naming collisions with repeated calls, though.

Notes

Use parameter names instead of the outdated ALIAS command.

To actually "default" to the current schema, use the simpler query I display. Using regclass does the trick automatically. Details:

In addition, this also avoids syntax errors and possible SQL injection from non-standard (or maliciously malformed) table names in your original code.

The code in your ELSE clause wouldn't work at all.

TABLE tbl; is basically short for SELECT * FROM tbl;.

Details on format() in the manual.

Bernini answered 30/5, 2014 at 14:33 Comment(5)
Would it be possible to return a type containing an id and jsonb where the jsonb contains dynamically built columns with values?Cuthburt
@Cuthburt Yes, no problem with that.Bernini
I'm looking at writing a fully dynamic cross-tab function, one that returns a columnar set of any length &/or type. However, it appears the only way to achieve output polymorphism is either a cursor FETCH ... INTO , or JSON row object. From what I've read so far JSON is the only type of object that can make the entire round trip. :-(Germaine
@L.Rodger: Basically, yes. It all depends ... Related: https://mcmap.net/q/744082/-execute-a-dynamic-crosstab-query, https://mcmap.net/q/28876/-dynamic-alternative-to-pivot-with-case-and-group-by, https://mcmap.net/q/28848/-refactor-a-pl-pgsql-function-to-return-the-output-of-various-select-queries ...Bernini
TY @ErwinBrandstetter for those links! Am currently reading your post re: polymorphic anyElement. Ye gads, SQL type impedances make for a puzzling maze! I can't imagine how long it took you to tease all this out. o: I see there's a user interest in being able to SELECT * straight out of a cursor for similar reasons. I know we'd all like to be able to cast to & fro freely, with record/row types interconvertible, but perhaps that'd violate a cardinal rule (or ten) about type strictness....? It does rather seem a long excursion to have to route through JSON to get there. :DGermaine

© 2022 - 2024 — McMap. All rights reserved.