Conditionally chose the set to iterate over in a FOR LOOP - postgresql

I would like to know if I can do a if-elsif or case-when inside a loop. No between the for-loop, but as a statament of the foor-loop to choose between a select or another.
something like this:
I have tried with both, if-elsif and case-when....but none of them worked, and I have been lurking around the net to find something but nope.
CREATE OR REPLACE FUNCTION myfunct(op integer, -vars-)
RETURNS table(-vars-)
LANGUAGE plpgsql
AS $function$
DECLARE
selectop record;
-vars-
BEGIN
FOR selectop in (IF (op=1) THEN
SELECT * FROM mytab WHERE somevar=true;
ELSIF (op=2) THEN
SELECT * from mytab WHERE somevar=false;
END IF;)
-things-
RETURN NEXT;
LOOP
---THINGS---
END;
$function$

I wonder why you don't just set a variable and use that in the query. That seems far easier. But from the academic point of view, it's an interesting question.
The problem is that IF or CASE as a control structure don't return something or evaluate to something. So we cannot use them in such a way.
If we think about returning something, then we may think of functions. So you could place a function call there, that returns a different set depending on an argument. But it seems like the function you want to have should implement exactly that, so this would just shift the problem to another level and ultimately ad infinitum.
So let's think about evaluating to something. Expressions come to our mind and indeed there is CASE as an expression, which we could use to switch. The only thing is, at least as far as I know, that cannot handle sets.
But it can handle arrays. So the idea is to use a CASE expression, that evaluates to (two) different arrays.
We will use the fact, that each table also defines a type in Postgres, that is the type of its rows. So we'll iterate over an array of that type.
The next neat thing in Postgres is, that we can use array_agg() to aggregate the complete table or a subset of it into an array. That's how we'll create the arrays we iterate over.
To iterate over the array we'll use a FOREACH loop. (Yes that's not a FOR loop over a cursor but semantically I'd guess that's close enough.)
Such a function could look like the following. elbat is our table and nmuloc4 that column of it we want to compare a value to, depending on the value of the function's argument switch. The function returns a SETOF elbat, that is a set of records from elbat.
CREATE FUNCTION noitcnuf
(switch integer)
RETURNS SETOF elbat
AS
$$
DECLARE
elbat_array elbat[];
elbat_element elbat;
BEGIN
FOREACH elbat_element IN ARRAY(CASE
WHEN switch = 1 THEN
(SELECT array_agg(elbat)
FROM elbat
WHERE nmuloc4 = true)
WHEN switch = 2 THEN
(SELECT array_agg(elbat)
FROM elbat
WHERE nmuloc4 = false)
ELSE
ARRAY[]::elbat[]
END) LOOP
RETURN NEXT elbat_element;
END LOOP;
RETURN;
END;
$$
LANGUAGE plpgsql;
db<>fiddle
In the ELSE branch of the CASE we just have an empty array of the right type. Otherwise we'd receive an error if we pass an argument to the function for which no branch in the CASE exits. Like that, we just get the empty set in such a case.
Note that this would also work for a view or probably even a set returning function instead of a table (I didn't explicitly test the latter).
But I'd also like to warn, that I suspect this approach to be likely less performant than just building up a query depending on variables and do a classic loop over a cursor or at best reduce it to a set based approach with no loops at all.

Related

PostgreSQL, how to retrieve k-th element from a Record Type?

For example, I construct a Record type by
SELECT ROW(1, false, 'fat');
After that, I want to get the second element from it, i think it should be something like
SELECT ROW(1, false, 'fat')._2;
or
SELECT GetElement(ROW(1, false, 'fat'),2);
However, I don't find something like that in the document.
Moreover, I try to define one by myself, I tried to implement a C function
CREATE OR REPLACE FUNCTION GetElement(anynonarray)
RETURNS anyelement xxxxxxxxx;
However,when I call PG_RETURN_BOOL or PG_RETURN_DATUM to return the result, I find that the PG kernel always try to call a DatumGetPointer function on my datum and crash the database.
Currently I implemented multiple GetElementXXX functions with different return type to solve this problem, but I still want to know if I can find some function that can retrieve an element of any type.

How to wrap record_out() function?

I'd like to create an IMMUTABLE wrapper function as discussed by https://stackoverflow.com/a/11007216/14731 but it's not clear how to proceed. The above Stackoverflow answer provides the following example:
For example, given:
CREATE OR REPLACE FUNCTION public.immutable_unaccent(regdictionary, text)
RETURNS text LANGUAGE c IMMUTABLE PARALLEL SAFE STRICT AS
'$libdir/unaccent', 'unaccent_dict';
CREATE OR REPLACE FUNCTION public.f_unaccent(text)
RETURNS text LANGUAGE sql IMMUTABLE PARALLEL SAFE STRICT AS
$func$
SELECT public.immutable_unaccent(regdictionary 'public.unaccent', $1)
$func$;
I scanned all the libraries in lib/ and as far as I can tell none of them export functions related to record_out(). Any ideas?
The record_out() function is an internal built-in function. You can get its definition like this:
select pg_get_functiondef('record_out'::regproc);
pg_get_functiondef
----------------------------------------------------------
CREATE OR REPLACE FUNCTION pg_catalog.record_out(record)+
RETURNS cstring +
LANGUAGE internal +
STABLE PARALLEL SAFE STRICT +
AS $function$record_out$function$ +
I don't know for what purpose you want the wrapping function. It only remains to warn that such a change may bring unexpected results.

Alias for function or procedure name

I was running into the issue defined in the following article, and the first answer solved my main concern of naming a parameter the same name as a table column. My new concern is that my function/procedure parameters are widely used and the name of my functions/procedures are fairly detailed.
PL/pgSQL column name the same as variable
Is there a way to define an alias for a function or procedure name - to be used inside its body?
Current code:
CREATE OR REPLACE PROCEDURE dbo.PR_DeleteCrazyNamedItemByCrazyNamedID(in NamedID UUID)
LANGUAGE plpgsql AS
$BODY$
DECLARE
BEGIN
Delete from dbo.Table t where PR_DeleteCrazyNamedItemByCrazyNamedID.NamedID = t.NamedID;
...
Desired code:
CREATE OR REPLACE PROCEDURE dbo.PR_DeleteCrazyNamedItemByCrazyNamedID(in NamedID UUID) as proc
LANGUAGE plpgsql AS
$BODY$
DECLARE
BEGIN
Delete from dbo.Table t where proc.NamedID = t.NamedID;
...
Not directly. The function name seems to be visible as record containing input parameters inside the function body, but it is not accessible for an ALIAS as suggested in my referenced answer because it actually serves as outer label. The manual:
Note
There is actually a hidden “outer block” surrounding the body of any
PL/pgSQL function. This block provides the declarations of the
function's parameters (if any), as well as some special variables such
as FOUND (see Section 42.5.5). The outer block is labeled with the
function's name, meaning that parameters and special variables can be
qualified with the function's name.
But you can combine an ALIAS for function parameters with an outer block label (one nesting level below the built-in outer block labeled with the function name) like this:
General example with a function:
CREATE OR REPLACE FUNCTION weird_procedure_name(named_id int)
RETURNS TABLE (referenced_how text, input_value int) LANGUAGE plpgsql AS
$func$
<< proc >> -- outer label!
DECLARE
named_id ALIAS FOR named_id; -- sic!
BEGIN
RETURN QUERY VALUES
('weird_procedure_name.named_id', weird_procedure_name.named_id)
, ('proc.named_id', proc.named_id)
, ('named_id', named_id)
;
END
$func$;
SELECT * FROM weird_procedure_name(666);
referenced_how | input_value
:---------------------------- | ----------:
weird_procedure_name.named_id | 666
proc.named_id | 666
named_id | 666
db<>fiddle here
named_id ALIAS FOR named_id; seems to be pointless noise, but now the input parameter is accessible via block label - effectively doing what you ask for. (You might chose a different name while being at it.)
And I would certainly add a code comment explaining why label and alias are needed, lest the next smart developer should be tempted to remove either.
Applied to your example:
CREATE OR REPLACE PROCEDURE PR_DeleteCrazyNamedItemByCrazyNamedID(in NamedID UUID)
LANGUAGE plpgsql AS
$BODY$
<< proc >> -- !
DECLARE
NamedID ALIAS FOR NamedID; -- sic!
BEGIN
DELETE FROM dbo.tbl t WHERE t.NamedID = proc.NamedID; -- what you wanted !
END
$BODY$;
I would still much rather work with unique parameter names to begin with, so no qualification is required at all. I like to prefix all parameter names with underscore (like: _named_id) - and never do the same for other object names.

Can you set an array of dates as a constant in PL/pgSQL?

I'm not even sure if this can be done, but if I want to make a function that figures out some business time things, and I want to have a constant that represents holidays, can I do something like this?
CREATE OR REPLACE FUNCTION businesstime(start_time, end_time)
...
DECLARE
HOLIDAYS constant date[] := '{2019-01-01, 2019-07-04, 2019-11-28}'
BEGIN
-- do business time stuff with holidays
END
...
If so, how? I can't get past syntax errors and I'm not sure if it's because I'm doing it wrong or it's impossible.
Thanks
The semicolon in your example is missing.
This code is working on my computer:
do $$
declare d date[] default '{2017-01-01, 2018-01-01}';
begin
raise notice '%', d;
end;
$$;
there are four different syntaxes, but the result and performance will be almost same (maybe there can be very small performance differences that depends on usage):
-- string literal of unknown type with late implicit casting
d := '{2017-01-01, 2018-01-01}';
-- string literal of date[] type
d := _date '{2017-01-01, 2018-01-01}';
There is little bit hack - for date array type I have to use alternative type name _date. It is old convention - internal names of array types starts by prefix _.
-- string literal of unknown type with immediate explicit casting
d := '{2017-01-01, 2018-01-01}'::date[];
d := CAST('{2017-01-01, 2018-01-01}' AS date[]);
-- using array constructor with late implicit casting
d := ARRAY['2017-01-01', '2018-01-01'];
-- using array constructor with casting of array
d := ARRAY['2017-01-01', '2018-01-01']::date[];
-- using array constructor with immediate casting of field
d := ARRAY['2017-01-01'::date, '2018-01-01'];
Type of first element forces types of other elements of array
There are more ways how to write array constant - but the differences between mentioned ways are for almost use cases almost zero.
Welp, finally got it. Just needed to cast:
HOLIDAYS constant date[] := '{2019-01-01, 2019-07-04, 2019-11-28}'::date[]
Still not sure if this is the best way to do it though, so not going to accept my own answer just yet. Would love to know if this is really the best/only way.
OK, it is very simple to do this in PostgreSQL just with using generate_series and array_agg functions, as below is the PL/PGSQL function definition and the example to use:
create or replace function generate_series_dates(varchar, varchar) returns date[]
As
'with series_dates as (
select generate_series($1::date, $2::date, ''1 days''::interval)::date as date
)
select array_agg(date) from series_dates'
LANGUAGE SQL
IMMUTABLE;
CREATE FUNCTION
select generate_series_dates('2018-01-01','2018-01-05');
generate_series_dates
----------------------------------------------------------
{2018-01-01,2018-01-02,2018-01-03,2018-01-04,2018-01-05}
(1 row)

Using variables in a postgres sql block

Can you have an SQL block that accepts a variable for input, uses that variable in a join and returns a result set.
The kicker is that I have been asked to do this outside a function - i.e. within an SQL block.
So, for example, I want to pass the value 1234567890 to the variable v_hold:
DO $$
declare
v_hold integer;
BEGIN
select * from t_table_A where ID = v_hold ;
--return alert_mesg;
END$$;
--$$ LANGUAGE plpgsql
The testing I've done says that in order to return a result set, you have to define that in a RETURN TABLE declaration. I've tried to define this outside a function, but I haven't figured it out.
Can this be done outside a function - i.e pass a variable and return a result set based on a select statement which references a variable in the where clause?
You could try using a prepared statement. For example:
PREPARE myStatement (int) AS SELECT * FROM t_table_A where ID = $1;
To then run the statement use the execute command:
EXECUTE myStatement(1234567890);
From the documentation:
DO executes an anonymous code block, or in other words a transient anonymous function in a procedural language.
The code block is treated as though it were the body of a function with no parameters, returning void. It is parsed and executed a single time.
You could generate your code block with a shell script and get the sort of effect you are looking for.