Replacing Placeholder values with another table's data - sql

I have 2 tables .The first table contains rows with placeholders and the second table contains those placeholders values.
I want a query which fetches data from the first table and replaces placeholders with actual values which are stored in the second table.
Ex:
Table1 Data
id value
608CB424-90BF-4B08-8CF8-241C7635434F jdbc:postgresql://{POSTGRESIP}:{POSTGRESPORT}/{TESTDB}
CDA4C3D4-72B5-4422-8071-A29D32BD14E0 https://{SERVICEIP}/svc/{TESTSERVICE}/
Table2 Data
id placeolder value
201FEBFE-DF92-4474-A945-A592D046CA02 POSTGRESIP 1.2.3.4
20D9DE14-643F-4CE3-B7BF-4B7E01963366 POSTGRESPORT 5432
45611605-F2D9-40C8-8C0C-251E300E183C TESTDB mytest
FA8E2E4E-014C-4C1C-907E-64BAE6854D72 SERVICEIP 10.90.30.40
45B76C68-8A0F-4FD3-882F-CA579EC799A6 TESTSERVICE mytest-service
Required output is
id value
608CB424-90BF-4B08-8CF8-241C7635434F jdbc:postgresql://1.2.3.4:5432/mytest
CDA4C3D4-72B5-4422-8071-A29D32BD14E0 https://10.90.30.40/svc/mytest-service/

If you want to use Python-like named placeholders then you need the helper function written on plpythonu:
create extension plpythonu;
create or replace function formatpystring( str text, a json ) returns text immutable language plpythonu as $$
import json
d = json.loads(a)
return str.format(**d)
$$;
Then simple test:
select formatpystring('{foo}.{bar}', '{"foo": "win", "bar": "amp"}');
formatpystring
----------------
win.amp
Finally you need to compose those arguments from your tables. It is simple:
select t1.id, formatpystring(t1.value, json_object_agg(t2.placeholder, t2.value)) as value
from table1 as t1, table2 as t2
group by t1.id, t1.value;
(Query was not tested but you have the direction)

(Clumsy) dynamic SQL implementation, featuring an outer join, but generating a recursive function call:
This function will not be very efficient, but probably the translation table is relatively small.
CREATE TABLE xlat_table (aa text ,bb text);
INSERT INTO xlat_table (aa ,bb ) VALUES( 'BBB', '/1.2.3.4/')
,( 'ccc', 'OMG') ,( 'ddd', '/4.3.2.1/') ;
CREATE FUNCTION dothe_replacements(_arg1 text) RETURNS text
AS
$func$
DECLARE
script text;
braced text;
res text;
found record; -- (aa text, bb text, xx text);
BEGIN
script := '';
res := format('%L', _arg1);
for found IN SELECT xy.aa,xy.bb
, regexp_matches(_arg1, '{\w+}','g' ) AS xx
FROM xlat_table xy
LOOP
-- RAISE NOTICE '#xx=%', found.xx[1];
-- RAISE NOTICE 'aa=%', found.aa;
-- RAISE NOTICE 'bb=%', found.bb;
braced := '{'|| found.aa || '}';
IF (found.xx[1] = braced ) THEN
-- RAISE NOTICE 'Res=%', res;
script := format ('replace(%s, %L, %L)'
,res,braced,found.bb);
res := format('%s', script);
END IF;
END LOOP;
if(length(script) =0) THEN return res; END IF;
script :='Select '|| script;
-- RAISE NOTICE 'script=%', script;
EXECUTE script INTO res;
return res;
END;
$func$
LANGUAGE plpgsql;
SELECT dothe_replacements( 'aaa{BBB}ccc{ddd}eee' );
SELECT dothe_replacements( '{AAA}bbb{CCC}DDD}{EEE}' );
Results:
CREATE TABLE
INSERT 0 3
CREATE FUNCTION
dothe_replacements
-----------------------------
aaa/1.2.3.4/ccc/4.3.2.1/eee
(1 row)
dothe_replacements
--------------------------
'{AAA}bbb{CCC}DDD}{EEE}'
(1 row)
The above method has quadratic behaviour(wrt the numberof xlat-entries); which is horrible.
But,we could dynamically create a function (once) and call it multiple times
(a poor man's generator)
Selecting only the relevant entries from the xlat table should probably be added.
And, you should of course re-create the function everytime the xlat table is changed.
CREATE FUNCTION create_replacement_function(_name text) RETURNS void
AS
$func$
DECLARE
argname text;
res text;
script text;
braced text;
found record; -- (aa text, bb text, xx text);
BEGIN
script := '';
argname := '_arg1';
res :=format('%I', argname);
for found IN SELECT xy.aa,xy.bb
FROM xlat_table xy
LOOP
-- RAISE NOTICE 'aa=%', found.aa;
-- RAISE NOTICE 'bb=%', found.bb;
-- RAISE NOTICE 'Res=%', res;
braced := '{'|| found.aa || '}';
script := format ('replace(%s, %L, %L)'
,res,braced,found.bb);
res := format('%s', script);
END LOOP;
script :=FORMAT('CREATE FUNCTION %I (_arg1 text) RETURNS text AS
$omg$
BEGIN
RETURN %s;
END;
$omg$ LANGUAGE plpgsql;', _name, script);
RAISE NOTICE 'script=%', script;
EXECUTE script ;
return ;
END;
$func$
LANGUAGE plpgsql;
SELECT create_replacement_function( 'my_function');
SELECT my_function('aaa{BBB}ccc{ddd}eee' );
SELECT my_function( '{AAA}bbb{CCC}DDD}{EEE}' );
And the result:
CREATE FUNCTION
NOTICE: script=CREATE FUNCTION my_function (_arg1 text) RETURNS text AS
$omg$
BEGIN
RETURN replace(replace(replace(_arg1, '{BBB}', '/1.2.3.4/'), '{ccc}', 'OMG'), '{ddd}', '/4.3.2.1/');
END;
$omg$ LANGUAGE plpgsql;
create_replacement_function
-----------------------------
(1 row)
my_function
-----------------------------
aaa/1.2.3.4/ccc/4.3.2.1/eee
(1 row)
my_function
------------------------
{AAA}bbb{CCC}DDD}{EEE}
(1 row)

The following offers a plpgsql solution in a with a single function.
You'll notice I've 'renamed' the value column. It's bad practice using rserved/key words as object names. Also soq is the schema I use for all SO code.
The process first takes the holder-values from table2 and generates a set of key-value pairs (in this case hstore, but jsonb would also work). It then builds an array from the value column (my column name: val_string) containing the place_holder name from the value. Finally, it iterates that array replacing the actual holder-name with the value from the key-values using the array value as the lookup key.
The performance would not be great with a larger volume from either table. If you need to process a large volume at a time to a single row temp table may yield better performance.
create or replace function soq.replace_holders( place_holder_line_in text)
returns text
language plpgsql
as $$
declare
l_holder_values hstore;
l_holder_line text;
l_holder_array text[];
l_indx integer;
begin
-- transform cloumns to key-value pairs of holder-value
select string_agg(place,',')::hstore
into l_holder_values
from (
select concat( '"',place_holder,'"=>"',place_value,'"') place
from soq.table2
) p;
-- raise notice 'holder_array_in==%',l_holder_values;
-- extract the text line and build array of place_holder names
select phv, string_to_array (string_agg(v,','),',')
into l_holder_line,l_holder_array
from (
select replace(replace(place_holder_line_in,'{',''),'}','') phv
, replace(replace(replace(regexp_matches(place_holder_line_in,'({[^}]+})','g')::text ,'{',''),'}',''),'"','') v
) s
group by phv;
-- raise notice 'Array==%',l_holder_array::text;
-- replace each key from text line with the corresponding value
for l_indx in 1 .. array_length(l_holder_array,1)
loop
l_holder_line = replace(l_holder_line,l_holder_array[l_indx],l_holder_values -> l_holder_array[l_indx]);
end loop;
-- done
return l_holder_line;
end;
$$;
-- Test driver
select id, soq.replace_holders(val_string) result_value from soq.table1;

I have created a simple query for this solution and it working as required.
WITH RECURSIVE cte(id, value, level) AS (
SELECT id,value, 0 as level
FROM Table1
UNION
SELECT ts.id,replace(ts.value,'{'||tp.placeholder||'}',tp.value) as value, level+1
FROM cte ts, Table2 tp WHERE ts.value LIKE CONCAT('%',tp.placeholder, '%')
)
SELECT id, value FROM cte c
where level =
(
select Max(level)
from cte c2 where c.id=c2.id
)
Output is
id value
CDA4C3D4-72B5-4422-8071-A29D32BD14E0 https://10.90.30.40/svc/mytest-service/
608CB424-90BF-4B08-8CF8-241C7635434F jdbc:postgresql://1.2.3.4:5432/mytest

Related

Multiple ALTER TABLE ADD COLUMN in one SQL function call

I came across some weird behaviour I'd like to understand.
I create a plpgsql function doing nothing except of ALTER TABLE ADD COLUMN. I call it 2 times on the same table:
A) In a single SELECT sentence
B) In a SQL function with same SELECT as in A)
Results are different: A) creates two columns, while B) creates only one column. Why?
Code:
CREATE FUNCTION add_text_column(table_name text, column_name text) RETURNS VOID
LANGUAGE plpgsql
AS $fff$
BEGIN
EXECUTE '
ALTER TABLE ' || table_name || '
ADD COLUMN ' || column_name || ' text;
';
END;
$fff$
;
-- this function is called only in B
CREATE FUNCTION add_many_text_columns(table_name text) RETURNS VOID
LANGUAGE SQL
AS $fff$
WITH
col_names (col_name) AS (
VALUES
( 'col_1' ),
( 'col_2' )
)
SELECT add_text_column(table_name, col_name)
FROM col_names
;
$fff$
;
-- A)
CREATE TABLE a (id integer);
WITH
col_names (col_name) AS (
VALUES
( 'col_1' ),
( 'col_2' )
)
SELECT add_text_column('a', col_name)
FROM col_names
;
SELECT * FROM a;
-- B)
CREATE TABLE b (id integer);
SELECT add_many_text_columns('b');
SELECT * FROM b;
Result:
CREATE FUNCTION
CREATE FUNCTION
CREATE TABLE
add_text_column
-----------------
(2 rows)
id | col_1 | col_2
----+-------+-------
(0 rows)
CREATE TABLE
add_many_text_columns
-----------------------
(1 row)
id | col_1
----+-------
(0 rows)
I'm using PostgreSQL 10.4. Please note that this is only a minimal working example, not the full functionality I need.
CREATE OR REPLACE FUNCTION g(i INTEGER)
RETURNS VOID AS $$
BEGIN
RAISE NOTICE 'g called with %', i;
END
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION t(i INTEGER)
RETURNS VOID AS $$
SELECT g(id)
FROM generate_series(1, i) id;
$$ LANGUAGE SQL;
What do you think happens when I run SELECT t(4)? The only statement printed from g() is g called with 1.
The reason for this is your add_many_text_columns function returns a single result (void). Because it's SQL and is simply returning the result of a SELECT statement, it seems to stop executing after getting the first result, which makes sense if you think of it - it can only return one result after all.
Now change the function to:
CREATE OR REPLACE FUNCTION t(i INTEGER)
RETURNS SETOF VOID AS $$
SELECT g(id)
FROM generate_series(1, i) id;
$$ LANGUAGE SQL;
And run SELECT t(4) again, and now this is printed:
g called with 1
g called with 2
g called with 3
g called with 4
Because the function now returns SETOF VOID, it doesn't stop after the first result and executes it fully.
So back to your functions, you could change your SQL function to return SETOF VOID, but it doesn't really make much sense - better I think to change it to plpgsql and have it do a PERFORM:
CREATE OR REPLACE FUNCTION t(i INTEGER)
RETURNS VOID AS $$
BEGIN
PERFORM g(id)
FROM generate_series(1, i) id;
END
$$ LANGUAGE plpgsql;
That will execute the statement fully and it still returns a single VOID.
eurotrash provided a good explanation.
Alternative solution 1
CREATE OR REPLACE FUNCTION t(i INTEGER)
RETURNS VOID AS
$func$
SELECT g(id)
FROM generate_series(1, i) id;
SELECT null::void;
$func$ LANGUAGE sql;
Because, quoting the manual:
SQL functions execute an arbitrary list of SQL statements, returning
the result of the last query in the list. In the simple (non-set)
case, the first row of the last query's result will be returned.
By adding a dummy SELECT at the end we avoid that Postgres stops after processing the the first row of the query with multiple rows.
Alternative solution 2
CREATE OR REPLACE FUNCTION t(i INTEGER)
RETURNS VOID AS
$func$
SELECT count(g(id))
FROM generate_series(1, i) id;
$func$ LANGUAGE sql;
By using an aggregate function, all underlying rows are processed in any case. The function returns bigint (that's what count() returns), so we get the number of rows as result.
Alternative solution 3
If you need to return void for some unknown reason, you can cast:
CREATE OR REPLACE FUNCTION t(i INTEGER)
RETURNS VOID AS
$func$
SELECT count(g(id))::text::void
FROM generate_series(1, i) id;
$func$ LANGUAGE sql;
The cast to text is a stepping stone because the cast from bigint to void is not defined.

Create stored procedure (int[] as param) which deletes existing records in table when there is no match in int array

ex: if i have sent 1,2,3 params to stored procedure with idxyz, then table has 1,2,3,4,5 ids then 4,5 should be deleted from table.
CREATE OR REPLACE FUNCTION example_array_input(INT[]) RETURNS SETOF ids AS
$BODY$
DECLARE
in_clause ALIAS FOR $1;
clause TEXT;
rec RECORD;
BEGIN
FOR rec IN SELECT id FROM ids WHERE id = ANY(in_clause)
LOOP
RETURN NEXT rec;
END LOOP;
-- final return
RETURN;
END
$BODY$ language plpgsql;
ex: SELECT * FROM example_array_input('{1,2,4,5,6}'::INT[]);
if existing table has 1,2,3,4,5,6,7,8,9. then it should delete 7,8,9 from that table since these are not there in the input array
You can use a DELETE statement like this for your purpose.
DELETE FROM ids
where id NOT IN ( select UNNEST('{1,2,4,5,6}'::INT[]) ) ;
DEMO
You can use a sql function that returns the deleted ids:
CREATE OR REPLACE FUNCTION example_array_input(in_clause INT[]) RETURNS SETOF ids
language sql
AS
$SQL$
DELETE
FROM ids
WHERE id NOT IN ( SELECT unnest(in_clause) )
RETURNING id;
$SQL$;
You can see a running example in http://rextester.com/PFG55537
In a 1 to 10 table running
SELECT * FROM example_array_input('{1,2,4,5,6}'::INT[]);
you obtain:

A PL/pgSQL function with varying return type (and varying inner query)

The data
Suppose I have the following data:
create temp table my_data1 (
id serial, val text
);
create temp table my_data2 (
id serial, val int
);
insert into my_data1(id, val)
values (default, 'a'), (default, 'c'), (default, 'd'), (default, 'b');
insert into my_data2(id, val)
values (default, 1), (default, 3), (default, 4), (default, 2);
The problem
I would like to write a plpgsql function which has 2 arguments: tbl (taking values my_data1 or my_data2) and order_by (which can be id or val or null). The function should fetch all rows from the table specified in tbl and order them by the column specified in order_by.
Below there are 2 solutions I have found (see also sqlfiddle). The question is which of them is preferable, and if there exists an even better solution.
Solution using temp table
I came up with the following workaround:
create function my_work(tbl text, order_by text default null)
returns text as
$my_work$
declare
q text;
begin
q := 'select * from ' || quote_ident(tbl);
if order_by is not null then
q := q || ' order by ' || quote_ident(order_by);
end if;
return q;
end
$my_work$ language plpgsql;
create function my_fetch(_query text, into_table text)
returns void as
$my_fetch$
begin
execute format($$
create temp table %I
on commit drop
as %s
$$, quote_ident(into_table), _query);
end
$my_fetch$ language plpgsql;
Then it remains to execute the following lines (preferably surrounded with 'begin/commit'):
select my_fetch(my_work('my_data1','id'), 'my_tmp');
select * from my_tmp;
Are there any negative side effects in this solution, e.g. is creating a temp table costy?
Another solution (using pg_typeof)
I've also read a great post on various approaches to dynamic queries with varying results. From the options mentioned there it seems the following is the best solution for my situation:
create or replace function not_my_work(_tbl_type anyelement, order_by text default null)
returns setof anyelement as
$func$
declare
q text;
begin
q := format('
select *
from %s
', pg_typeof(_tbl_type));
if order_by is not null then
q := q || ' order by ' || quote_ident(order_by);
end if;
return query execute q;
end
$func$ language plpgsql;
select not_my_work(null::my_data1, 'id');
Does this approach have any advantages over the approach using temp table?
I have two comments to the first solution.
First, use or %I or quote_ident() in format() function, not both. Compare:
with q(s) as (
values ('abba'), ('ABBA')
)
select
quote_ident(s) ok1,
format('%I', s) ok2,
format('%I', quote_ident(s)) bad_idea
from q;
ok1 | ok2 | bad_idea
--------+--------+------------
abba | abba | abba
"ABBA" | "ABBA" | """ABBA"""
(2 rows)
Second, you do not need two functions:
create or replace function my_select(into_table text, tbl text, order_by text default null)
returns void as $function$
declare
q text;
begin
q := 'select * from ' || quote_ident(tbl);
if order_by is not null then
q := q || ' order by ' || order_by;
end if;
execute format($$
create temp table %I
on commit drop
as %s
$$, into_table, q);
end
$function$ language plpgsql;
begin;
select my_select('my_tmp', 'my_data1', 'id');
select * from my_tmp;
commit;
BEGIN
my_select
-----------
(1 row)
id | val
----+-----
1 | a
2 | c
3 | d
4 | b
(4 rows)
COMMIT
In this particular case, the second solution is better.
A temporary table is not particularly expensive, but still unnecessary.
The cost will be the more important the more data in the table.
If you have a good alternative to create a temporary table, use it.
Besides, the need to include the function call and the select query in a transaction can be a bit cumbersome in some cases.
The second solution is smart and is ideally suited to the task at hand.

Safe way to open cursor with dynamic column name from user input

I am trying write function which open cursor with dynamic column name in it.
And I am concerned about obvious SQL injection possibility here.
I was happy to see in the fine manual that this can be easily done, but when I try it in my example, it goes wrong with
error: column does not exist.
My current attempt can be condensed into this SQL Fiddle. Below, I present formatted code for this fiddle.
The goal of tst() function is to be able to count distinct occurances of values in any given column of constant query.
I am asking for hint what am I doing wrong, or maybe some alternative way to achieve the same goal in a safe way.
CREATE TABLE t1 (
f1 character varying not null,
f2 character varying not null
);
CREATE TABLE t2 (
f1 character varying not null,
f2 character varying not null
);
INSERT INTO t1 (f1,f2) VALUES ('a1','b1'), ('a2','b2');
INSERT INTO t2 (f1,f2) VALUES ('a1','c1'), ('a2','c2');
CREATE OR REPLACE FUNCTION tst(p_field character varying)
RETURNS INTEGER AS
$BODY$
DECLARE
v_r record;
v_cur refcursor;
v_sql character varying := 'SELECT count(DISTINCT(%I)) as qty
FROM t1 LEFT JOIN t2 ON (t1.f1=t2.f1)';
BEGIN
OPEN v_cur FOR EXECUTE format(v_sql,lower(p_field));
FETCH v_cur INTO v_r;
CLOSE v_cur;
return v_r.qty;
END;
$BODY$
LANGUAGE plpgsql;
Test execution:
SELECT tst('t1.f1')
Provides error message:
ERROR: column "t1.f1" does not exist
Hint: PL/pgSQL function tst(character varying) line 1 at OPEN
This would work:
SELECT tst('f1');
The problem you are facing: format() interprets parameters concatenated with %I as one identifier. You are trying to pass a table-qualified column name that consists of two identifiers, which is interpreted as "t1.f1" (one name, double-quoted to preserve the otherwise illegal dot in the name.
If you want to pass table and column name, use two parameters:
CREATE OR REPLACE FUNCTION tst2(_col text, _tbl text = NULL)
RETURNS int AS
$func$
DECLARE
v_r record;
v_cur refcursor;
v_sql text := 'SELECT count(DISTINCT %s) AS qty
FROM t1 LEFT JOIN t2 USING (f1)';
BEGIN
OPEN v_cur FOR EXECUTE
format(v_sql, CASE WHEN _tbl <> '' -- rule out NULL and ''
THEN quote_ident(lower(_tbl)) || '.' ||
quote_ident(lower(_col))
ELSE quote_ident(lower(_col)) END);
FETCH v_cur INTO v_r;
CLOSE v_cur;
RETURN v_r.qty;
END
$func$ LANGUAGE plpgsql;
Aside: It's DISTINCT f1- no parentheses around the column name, unless you want to make it a row type.
Actually, you don't need a cursor for this at all. Faster, simpler:
CREATE OR REPLACE FUNCTION tst3(_col text, _tbl text = NULL, OUT ct bigint) AS
$func$
BEGIN
EXECUTE format('SELECT count(DISTINCT %s) AS qty
FROM t1 LEFT JOIN t2 USING (f1)'
, CASE WHEN _tbl <> '' -- rule out NULL and ''
THEN quote_ident(lower(_tbl)) || '.' ||
quote_ident(lower(_col))
ELSE quote_ident(lower(_col)) END)
INTO ct;
RETURN;
END
$func$ LANGUAGE plpgsql;
I provided NULL as parameter default for convenience. This way you can call the function with just a column name or with column and table name. But not without column name.
Call:
SELECT tst3('f1', 't1');
SELECT tst3('f1');
SELECT tst3(_col := 'f1');
Same as for test2().
SQL Fiddle.
Related answer:
Table name as a PostgreSQL function parameter

Can I have a postgres plpgsql function return variable-column records?

I want to create a postgres function that builds the set of columns it
returns on-the-fly; in short, it should take in a list of keys, build
one column per-key, and return a record consisting of whatever that set
of columns was. Briefly, here's the code:
CREATE OR REPLACE FUNCTION reports.get_activities_for_report() RETURNS int[] AS $F$
BEGIN
RETURN ARRAY(SELECT activity_id FROM public.activity WHERE activity_id NOT IN (1, 2));
END;
$F$
LANGUAGE plpgsql
STABLE;
CREATE OR REPLACE FUNCTION reports.get_amount_of_time_query(format TEXT, _activity_id INTEGER) RETURNS TEXT AS $F$
DECLARE
_label TEXT;
BEGIN
SELECT label INTO _label FROM public.activity WHERE activity_id = _activity_id;
IF _label IS NOT NULL THEN
IF lower(format) = 'percentage' THEN
RETURN $$TO_CHAR(100.0 *$$ ||
$$ (SUM(CASE WHEN activity_id = $$ || _activity_id || $$ THEN EXTRACT(EPOCH FROM ended - started) END) /$$ ||
$$ SUM(EXTRACT(EPOCH FROM ended - started))),$$ ||
$$ '990.99 %') AS $$ || quote_ident(_label);
ELSE
RETURN $$SUM(CASE WHEN activity_id = $$ || _activity_id || $$ THEN ended - started END)$$ ||
$$ AS $$ || quote_ident(_label);
END IF;
END IF;
END;
$F$
LANGUAGE plpgsql
STABLE;
CREATE OR REPLACE FUNCTION reports.build_activity_query(format TEXT, activities int[]) RETURNS TEXT AS $F$
DECLARE
_activity_id INT;
query TEXT;
_activity_count INT;
BEGIN
_activity_count := array_upper(activities, 1);
query := $$SELECT agent_id, portal_user_id, SUM(ended - started) AS total$$;
FOR i IN 1.._activity_count LOOP
_activity_id := activities[i];
query := query || ', ' || reports.get_amount_of_time_query(format, _activity_id);
END LOOP;
query := query || $$ FROM public.activity_log_final$$ ||
$$ LEFT JOIN agent USING (agent_id)$$ ||
$$ WHERE started::DATE BETWEEN actual_start_date AND actual_end_date$$ ||
$$ GROUP BY agent_id, portal_user_id$$ ||
$$ ORDER BY agent_id$$;
RETURN query;
END;
$F$
LANGUAGE plpgsql
STABLE;
CREATE OR REPLACE FUNCTION reports.get_agent_activity_breakdown(format TEXT, start_date DATE, end_date DATE) RETURNS SETOF RECORD AS $F$
DECLARE
actual_end_date DATE;
actual_start_date DATE;
query TEXT;
_rec RECORD;
BEGIN
actual_start_date := COALESCE(start_date, '1970-01-01'::DATE);
actual_end_date := COALESCE(end_date, now()::DATE);
query := reports.build_activity_query(format, reports.get_activities_for_report());
FOR _rec IN EXECUTE query LOOP
RETURN NEXT _rec;
END LOOP;
END
$F$
LANGUAGE plpgsql;
This builds queries that look (roughly) like this:
SELECT agent_id,
portal_user_id,
SUM(ended - started) AS total,
SUM(CASE WHEN activity_id = 3 THEN ended - started END) AS "Label 1"
SUM(CASE WHEN activity_id = 4 THEN ended - started END) AS "Label 2"
FROM public.activity_log_final
LEFT JOIN agent USING (agent_id)
WHERE started::DATE BETWEEN actual_start_date AND actual_end_date
GROUP BY agent_id, portal_user_id
ORDER BY agent_id
When I try to call the get_agent_activity_breakdown() function, I get this error:
psql:2009-10-22_agent_activity_report_test.sql:179: ERROR: a column definition list is required for functions returning "record"
CONTEXT: SQL statement "SELECT * FROM reports.get_agent_activity_breakdown('percentage', NULL, NULL)"
PL/pgSQL function "test_agent_activity" line 92 at SQL statement
The trick is, of course, that the columns labeled 'Label 1' and 'Label
2' are dependent on the set of activities defined in the contents of the
activity table, which I cannot predict when calling the function. How
can I create a function to access this information?
If you really want to create such table dynamically, maybe just create a temporary table within the function so it can have any columns you want. Let the function insert all rows into the table instead of returning them. The function can return only the name of the table or you can just have one exact table name that you know. After running that function you can just select data from the table. The function should also check if the temporary table exists so it should delete or truncate it.
Simon's answer might be better overall in the end, I'm just telling you how to do it without changing what you've got.
From the docs:
from_item can be one of:
...
function_name ( [ argument [, ...] ] ) [ AS ] alias [ ( column_alias [, ...] | column_definition [, ...] ) ]
function_name ( [ argument [, ...] ] ) AS ( column_definition [, ...] )
In other words, later it says:
If the function has been defined as
returning the record data type, then
an alias or the key word AS must be
present, followed by a column
definition list in the form (
column_name data_type [, ... ] ). The
column definition list must match the
actual number and types of columns
returned by the function.
I think the alias thing is only an option if you've predefined a type somewhere (like if you're mimicing the output of a predefined table, or have actually used CREATE TYPE...don't quote me on that, though.)
So, I think you would need something like:
SELECT *
FROM reports.get_agent_activity_breakdown('percentage', NULL, NULL)
AS (agent_id integer, portal_user_id integer, total something, ...)
The problem for you lies in the .... You'll need to know before you execute the query the names and types of all the columns--so you'll end up selecting on public.activity twice.
Both Simon's and Kev's answers are good ones, but what I ended up doing was splitting the calls to the database into two queries:
Build the query using the query constructor methods I included in the question, returning that to the application.
Call the query directly, and return that data.
This is safe in my case because the dynamic column list is not subject to frequent change, so I don't need to worry about the query's target data changing in between these calls. Otherwise, though, my method might not work.
you cannot change number of output columns, but you can to use refcursor, and you can return opened cursor.
more on http://okbob.blogspot.com/2008/08/using-cursors-for-generating-cross.html