postgresqlplpgsqldynamicquery

Returning dynamic query results from PL/PGSQL function, multiple rows


I work on the following test table:

select * from test;
 id | broker  | affiliate | leads 
----+---------+-----------+-------
  1 | broker1 | aff1      |     1
  2 | broker1 | aff2      |     2
  3 | broker2 | aff2      |     4
(3 rows)

I want to create a function which will accept group by column name and where predicate. Here is a working example of the function:

DROP FUNCTION report2(TEXT,TEXT);
CREATE OR REPLACE FUNCTION report2(
    group_by_column_name TEXT,
    sql_where TEXT
)
RETURNS RECORD
AS $$
DECLARE
    query TEXT;
    result_row RECORD;
BEGIN
    query := 'SELECT ';
    IF group_by_column_name <> '' THEN
        query := query || group_by_column_name || ', ';
    END IF;
    query := query || 'sum(leads) FROM test';
    IF sql_where <> '' THEN
        query := query || ' WHERE ' || sql_where;
    END IF;
    IF group_by_column_name <> '' THEN
        query := query || ' GROUP BY(' || group_by_column_name || ')';
    END IF;
    RAISE NOTICE 'query: %;', query;

    

    EXECUTE query INTO result_row;
    RETURN result_row;

END
$$ LANGUAGE 'plpgsql';

and it's usage:

select report2('broker', '');
NOTICE:  query: SELECT broker, sum(leads) FROM test GROUP BY(broker);
   report2   
-------------
 (broker1,3)
(1 row)

I'm trying to get not only a first row of the query but all the rows (obviously).

I tried the following:

DROP FUNCTION report2(TEXT,TEXT);
CREATE OR REPLACE FUNCTION report2(
    group_by_column_name TEXT,
    sql_where TEXT
)
RETURNS SETOF RECORD
AS $$
DECLARE
    query TEXT;
    result_row RECORD;
BEGIN
    query := 'SELECT ';
    IF group_by_column_name <> '' THEN
        query := query || group_by_column_name || ', ';
    END IF;
    query := query || 'sum(leads) FROM test';
    IF sql_where <> '' THEN
        query := query || ' WHERE ' || sql_where;
    END IF;
    IF group_by_column_name <> '' THEN
        query := query || ' GROUP BY(' || group_by_column_name || ')';
    END IF;
    RAISE NOTICE 'query: %;', query;

--  Does not worK:
--     FOR result_row IN EXECUTE query
--     LOOP
--         RETURN NEXT result_row;
--     END LOOP;

-- Does not worK:
--     RETURN QUERY EXECUTE query;



END
$$ LANGUAGE 'plpgsql';

In both cases, with FOR or RETURN QUERY, the error occurs:

NOTICE:  query: SELECT broker, sum(leads) FROM test GROUP BY(broker);
ERROR:  materialize mode required, but it is not allowed in this context
CONTEXT:  PL/pgSQL function report2(text,text) line 21 at RETURN NEXT

From the [docs][1]:

If multiple rows are returned, only the first will be assigned to the INTO variable(s).

So, how is it possible to get the entire results?



  [1]: https://(https://www.postgresql.org/docs/current/plpgsql-statements.html#PLPGSQL-STATEMENTS-EXECUTING-DYN)

Solution

  • I would return setof json instead of record because the number of result rows varies and json is more usable than record. So here is the function:

    create function report2(arg_group text default '', arg_where text default '')
    returns setof json language plpgsql as
    $$
    begin
      return query execute format(
        'select to_json(t) from (select %s sum(leads) from test %s %s) t', 
        case when arg_group = '' then '' else arg_group || ',' end,
        case when arg_where = '' then '' else 'where ' || arg_where end,
        case when arg_group = '' then '' else 'group by ' || arg_group end
       ); 
    end; 
    $$
    
    select report2('broker');
    
    report2
    {"broker":"broker1","sum":3}
    {"broker":"broker2","sum":4}