PostgreSQL add each item to list - DB functions - sql

I'd like to create a DB function that will accept a list of numbers and return a list of numbers. For each item in the list that was passed to the function, it should check some condition and add it to the response list. However, I don't think the way I am trying to do it is really a correct one. What I tried writing is basically some pseudo code here.
CREATE OR REPLACE FUNCTION map_numbers(numbers integer[])
returns integer[]
AS
$BODY$
DECLARE return_list integer[];
FOREACH field IN ARRAY numbers LOOP
CASE
WHEN field = 3 THEN -- add 43 (this was a random thought, but I am basically trying to map a few of the numbers to different values)
END
END LOOP;
RETURN QUERY SELECT * FROM return_list;
$BODY$
LANGUAGE sql VOLATILE
COST 100;

You need to put this into an IF statement. To append a value to an array use ||
CREATE OR REPLACE FUNCTION map_numbers(numbers integer[])
returns integer[]
AS
$BODY$
DECLARE
return_list integer[] := integer[];
BEGIN
FOREACH field IN ARRAY numbers LOOP
if field = 3 then
return_list := return_list || 43;
elsif field = 15 then
return_list := return_list || 42;
else
return_list := return_list || field;
end if;
END LOOP;
return return_list; --<< no SELECT required, just return the variable
END;
$BODY$
LANGUAGE plpgsql --<< you need PL/pgSQL, not SQL for the above
STABLE;
This can also be done using SQL rather than PL/pgSQL which usually is more efficient:
CREATE OR REPLACE FUNCTION map_numbers(numbers integer[])
returns integer[]
AS
$BODY$
select array_agg(field order by idx)
from (
select case
when field = 3 then 43
when field = 15 then 42
else field
end as field,
idx
from unnest(numbers) with ordinality as t(field, idx)
) x;
$BODY$
LANGUAGE sql
STABLE;

Related

PostgreSQL - Function with conditional local variable

I want to create a PostgreSQL function that will filter out table based on selected parameters.
However, I also need to perform some more complex logic based on the argument supplied. So I wanted to declare a local variable which would be based on some conditional logic.
What I have:
CREATE OR REPLACE FUNCTION get_something(parameter1 INT, parameter2 VARCHAR[])
DECLARE
conditional_variable := (
IF $1 = 50 THEN
'result-1'
ELSIF $1 = 100 THEN
ARRAY['result-2', 'result-3']
END IF;
)
RETURNS TABLE (
"time" BIGINT,
some_column NUMERIC
) AS $$
SELECT
time,
some_column
FROM "SomeNiceTable"
WHERE time = $1
AND some_dimension = ANY($2::VARCHAR[])
AND some_other_dimension = ANY(conditional_variable::VARCHAR[]);
$$ LANGUAGE SQL;
But it does not work this way. Is there a way how to achieve such thing?
You can not have DECLARE block and variables in a language sql function.
So you need to switch to language plpgsql and adjust the structure to be valid PL/pgSQL
CREATE OR REPLACE FUNCTION get_something(parameter1 INT, parameter2 VARCHAR[])
RETURNS TABLE ("time" BIGINT, some_column NUMERIC)
AS
$$
declare
conditional_variable text[];
begin
IF parameter1 = 50 THEN
conditional_variable := array['result-1'];
ELSIF parameter1 = 100 THEN
conditional_variable := ARRAY['result-2', 'result-3']
ELSE
????
END IF;
return query
SELECT
time,
some_column
FROM "SomeNiceTable"
WHERE time = $1
AND some_dimension = ANY($2::VARCHAR[])
AND some_other_dimension = ANY(conditional_variable);
END;
$$
LANGUAGE plpgsql;

Call an Array result from select function to do update function with postgresql

I'm new with SQL functions and postgreSQL. I just try to select some mails of my compte table to update them afterwards so I create select and update functions but I have always the error:
"ERROR: query "SELECT emailAnonymisation()" returned more than one row
CONTEXT: PL/pgSQL function updateMail() line 5 during statement block local variable initialization"
The problem is in my update function but I don't know if it's only a variable problem or a function logical problem...
My select function
CREATE OR REPLACE FUNCTION emailAnonymisation()
RETURNS table (mail varchar)
LANGUAGE plpgsql
AS $$
BEGIN
return query
SELECT compte.mail
FROM compte
limit 100;
END
$$;
My update function where I call the emailAnonymisation() function and where the problem is I think
CREATE OR REPLACE FUNCTION updateMail()
RETURNS varchar[] AS
$BODY$
DECLARE
_tbl varchar[]:=emailAnonymisation();
t text;
BEGIN
FOREACH t IN ARRAY _tbl
LOOP
EXECUTE '
UPDATE ' || t || '
SET t = REPLACE(SUBSTR(t,LOCATE('#',t) + 1),"X")
WHERE LOCATE('#',t) > 0;';
END LOOP;
END;
$BODY$ LANGUAGE plpgsql;
the update call
select updateMail();
Try using SETOF:
CREATE OR REPLACE FUNCTION emailAnonymisation()
RETURNS SETOF string(mail varchar)
LANGUAGE plpgsql
AS $$
BEGIN
return query
SELECT compte.mail
FROM compte
limit 100;
END
$$;
Ok I have finally found what was the problem with the select I should have a return like this with RETURNS character varying[]
CREATE OR REPLACE FUNCTION emailAnonymisation()
RETURNS character varying[]
AS $$
DECLARE
result character varying[];
BEGIN
SELECT ARRAY( SELECT compte.mail as mail
FROM compte
WHERE mail IS NOT NULL
limit 100)
into result;
return result;
END;
$$
LANGUAGE plpgsql;
And for the update the same type as the select function
CREATE OR REPLACE FUNCTION updateMail()
RETURNS character varying(150) AS
$BODY$
DECLARE
_tbl character varying[]:=emailAnonymisation();
mail text;
endUrl text := '#mail.com';
BeginningUrl text := random_string(15);
BEGIN
FOREACH mail IN ARRAY _tbl
LOOP
UPDATE ' || t || '
SET t = REPLACE(SUBSTR(t,LOCATE('#',t) + 1),"X")
WHERE LOCATE('#',t) > 0;';
END LOOP;

query has no destination for result data - PostgreSQL

I have created the following stored procedure in PostgreSQL:
CREATE OR REPLACE function incomingdel(IN del_ID varchar) RETURNS VOID AS $$
DECLARE xyz integer;
BEGIN
SELECT * INTO xyz FROM incomingcheck(cast(del_ID as integer));
IF xyz <> 1
THEN
SELECT * INTO xyz FROM perfdel(cast(del_ID as integer));
END IF;
END
$$ LANGUAGE plpgsql
even though the function returns VOID, I am still getting
Error: query has no destination for result data
when i execute the function. Can someone please help?
Thanks
When you do SELECT * INTO ... in a PL/pgSQL function you need to supply a variable of type record or of the row type of the row source, even if the function only returns an integer. The functions incomingcheck(int) and perfdel(int) are obviously returning a single integer in which case you can simplify your function:
CREATE FUNCTION incomingdel(del_ID integer) RETURNS VOID AS $$
DECLARE
xyz integer;
BEGIN
xyz := incomingcheck(del_ID);
IF xyz <> 1 THEN
xyz := perfdel(del_ID);
END IF;
-- Do something with xyz
END; $$ LANGUAGE plpgsql;
If the functionality is actually in the called functions (so you do not process the xyz value), then it becomes simpler still:
CREATE FUNCTION incomingdel(del_ID integer) RETURNS VOID AS $$
BEGIN
IF incomingcheck(del_ID) <> 1 THEN
perfdel(del_ID);
END IF;
END; $$ LANGUAGE plpgsql;

PostgreSQL execute + date

I am trying to create an SQL select statement in PL/pgSQL procedure. It was all fine until I had to add a date to the select. The problem is that to compare dates in select clause my date must be enclosed in single quotes '' (e.g. '01.01.2011'), but in my case it is already a text type and I can't add it there.
Below is a sample code that should have same problem:
CREATE OR REPLACE FUNCTION sample(i_date timestamp without time zone)
RETURNS integer AS
$BODY$
DECLARE
_count integer := 0;
_sql text := '';
BEGIN
IF i_date IS NOT NULL THEN
_cond := _cond || ' AND t.created>' || i_date;
END IF;
_sql := 'SELECT count(*) FROM test t WHERE 1=1' || _cond;
EXECUTE _sql INTO _count;
RETURN _count;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
Is there any other way to "escape" date? Or some other suggestions?
To use insert values safely and efficiently into a dynamically build and executed SQL string, there are a number of possibilities. The best one is to use the USING clause like this:
CREATE OR REPLACE FUNCTION sample(_date timestamp without time zone)
RETURNS integer AS
$BODY$
BEGIN
RETURN QUERY EXECUTE '
SELECT count(*)::int
FROM test t
WHERE t.created > $1'
USING _date;
END;
$BODY$ LANGUAGE plpgsql VOLATILE;
This is actually a bad example, because it could be simplified to:
CREATE OR REPLACE FUNCTION sample(_date timestamp)
RETURNS integer AS
$BODY$
SELECT count(*)::int
FROM test
WHERE created > $1;
$BODY$ LANGUAGE sql;
Another way would be to use quote_literal().
I wrote more about dynamic SQL in plpgsql here.

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