Create function in dynamic sql PostgreSQL - sql

Is it possible to create a function or execute anonymous block inside dynamic SQL in PostgreSQL?
I'm looking for something like this:
Create or replace FUNCTION fff(p1 int)
LANGUAGE plpgsql
AS $$
DECLARE
v_Qry VARCHAR(4000);
BEGIN
v_Qry := '
Create or replace FUNCTION fff_DYNAMIC_SQL()
LANGUAGE plpgsql
AS $$
DECLARE
v1 INTEGER;
begin
v1 := ' || p1 || ';
RETURN;
END; $$;';
EXECUTE v_Qry;
RETURN;
END; $$;

You have three levels of nested string in your code. The best way to deal with that, is to use dollar quoting for all of them. When creating dynamic SQL it's also better to use format() instead of string concatenation. Then you only need a single string with placeholders which makes the code a lot easier to read.
To nest multiple dollar quoted strings use a different delimiter each time:
Create or replace FUNCTION fff(p1 int)
returns void
LANGUAGE plpgsql
AS
$$ --<< outer level quote
DECLARE
v_Qry VARCHAR(4000);
BEGIN
v_Qry := format(
$string$ --<< quote for the string constant passed to the format function
Create or replace FUNCTION fff_DYNAMIC_SQL()
returns void
LANGUAGE plpgsql
AS
$f1$ --<< quoting inside the actual function body
DECLARE
v1 INTEGER;
begin
v1 := %s;
RETURN;
END;
$f1$
$string$, p1);
EXECUTE v_Qry;
RETURN;
END;
$$;
You also forgot to declare the returned data type. If the function does not return anything, you need to use returns void.

Related

How do I return a table from a function with a bespoke column name?

This function works:
CREATE OR REPLACE FUNCTION public.a()
RETURNS TABLE(a text)
LANGUAGE plpgsql
AS $function$
BEGIN
return query execute
'select a from ztable';
END;
$function$;
But when I try to add some text to the column name:
CREATE OR REPLACE FUNCTION public.a(prefix text)
RETURNS TABLE(a text)
LANGUAGE plpgsql
AS $function$
BEGIN
return query execute
'select a as $1_a from ztable' using prefix;
END;
$function$;
This just fails as a syntax error on $1.
Or:
CREATE OR REPLACE FUNCTION public.a(prefix text)
RETURNS TABLE(a text)
LANGUAGE plpgsql
AS $function$
BEGIN
return query execute
'select a as '||prefix||'_a from ztable';
END;
$function$;
select * from a('some prefix') doesn't work.
Is there some other syntax that does the job?
That's simply not possible. SQL does not allow dynamic column names.
You must assign a column alias with the call. Like:
SELECT a AS prefix_a FROM public.a();
Or in a column definition list directly attached to the function:
SELECT * FROM public.a() AS f(prefix_a);
Or, while dealing with a single output column, even just:
SELECT * FROM public.a() AS prefix_a;
See:
RETURNING rows using unnest()?

Postgres function: assiging results from built-in function to variable

How to assign results of REPLACE() built-in Postgres function to another variable?
I am trying something like this, but it does not work:
CREATE FUNCTION uri2text(uri text) RETURNS text AS $$
SET temp_text = SELECT REPLACE(uri , '%20', ' ');
RETURN temp_text;
$$ LANGUAGE SQL;
You can't have variables in SQL (which is the language that language sql selects for the function). To use variables, you need PL/pgSQL, and as documented in the manual assignment is done using := (or =) in PL/pgSQL. And you need to declare a variable before you can use it.
You also don't need a SELECT statement to call a function.
So if you do want to use PL/pgSQL, the function needs to look like this:
CREATE FUNCTION uri2text(uri text)
RETURNS text
AS $$
declare
temp_text text;
begin
temp_text := REPLACE(uri , '%20', ' ');
RETURN temp_text;
end;
$$ LANGUAGE plpgsql;
However you wouldn't even need a variable:
CREATE FUNCTION uri2text(uri text)
RETURNS text
AS $$
begin
RETURN REPLACE(uri , '%20', ' ');
end;
$$ LANGUAGE plpgsql;
And you don't even need PL/pgSQL for this:
CREATE FUNCTION uri2text(uri text)
RETURNS text
AS $$
SELECT REPLACE(uri , '%20', ' ');
$$ LANGUAGE sql;
Note that in case of the SQL function, you do need the SELECT because in SQL there is no return or assignment.

Generalize Get/Create Stored Procedure From One Item to Many

I have an express.js server running an application and from that server I can access or create "variant_id"s in PostgreSQL (Version 11) by using a stored procedure.
SELECT(get_or_create_variant_id(info_about_variant));
Sometimes I also need to get a bunch of these variant ids back by using a different stored procedure that takes multiple variants and returns multiple ids.
SELECT(get_or_create_variant_ids([info_about_variant, info_about_another_variant]));
What is the best way to generalize getting/creating a single id to doing multiple at once? I'm handling it in a LOOP in my stored procedure, but it feels like I should be able to use a JOIN instead.
CREATE OR REPLACE FUNCTION get_or_create_variant_id(
variant_in VARIANT_TYPE
) RETURNS INT AS $$
DECLARE variant_id_out INTEGER;
BEGIN
-- I'll be changing this to a ON CONFLICT block shortly
SELECT(get_variant_id(variant_in) INTO variant_id_out);
IF (variant_id_out IS NOT NULL) THEN
RETURN variant_id_out;
ELSE
INSERT INTO public.variant (
[some_fields]
)
VALUES (
[some_values]
)
RETURNING variant_id INTO variant_id_out;
RETURN variant_id_out;
END IF;
END;
$$ LANGUAGE plpgsql;
-- What is the best way to avoid a loop here?
CREATE OR REPLACE FUNCTION get_or_create_variant_ids(
variants_in VARIANT_TYPE []
) RETURNS INT [] AS $$
DECLARE variant_ids_out INTEGER [];
DECLARE variants_in_length INTEGER;
DECLARE current_variant_id INTEGER;
BEGIN
SELECT (array_length(variants_in, 1) INTO variants_in_length);
FOR i IN 1..variants_in_length LOOP
SELECT(get_or_create_variant_id(variants_in[i]) INTO current_variant_id);
SELECT(array_append(variant_ids_out, current_variant_id) INTO variant_ids_out);
END LOOP;
RETURN variant_ids_out;
END;
$$ LANGUAGE plpgsql;
-- Everything below is included for completeness, but probably less relevant to my question.
CREATE TYPE variant_type AS (
[lots of info about the variant]
);
CREATE OR REPLACE FUNCTION get_variant_id(
variant_in VARIANT_TYPE
) RETURNS INT AS $$
DECLARE variant_id_out INTEGER;
BEGIN
SELECT variant_id into variant_id_out
FROM public.variant
WHERE
[I want them to]
;
RETURN variant_id_out;
END;
$$ LANGUAGE plpgsql;
You can avoid explicit loop using builtin array functions - in this case, unnest function, and array constructor.
CREATE OR REPLACE FUNCTION get_or_create_variant_ids_v2(
variants_in VARIANT_TYPE []
)
RETURNS integer []
LANGUAGE sql AS $$
SELECT ARRAY(
SELECT get_or_create_variant_id(u.v)
FROM unnest(variants_in) AS u(v)
)
$$ LANGUAGE sql;

Evaluate dynamic condition in PL/pgSQL using EXECUTE

I have a function that needs to dynamically validate the input based on the value type. It does this by finding the constraint in another table, but for simplicity I provide the constraint as well in the function below.
This other table contains (value_type, value_constraint), where value_constraint is a text field that contains e.g. value::int > 0. I need to dynamically check this constraint within my insert function. I was trying to do that using EXECUTE as seen below, but it's not working.
How do I dynamically execute a condition statement and get the value as a boolean into v_successful_insert?
CREATE OR REPLACE FUNCTION insert_value(p_value_type text, p_value_constraint text, p_value text) RETURNS boolean
AS $$
DECLARE
v_successful_insert bool;
BEGIN
EXECUTE p_value_constraint INTO v_successful_insert;
IF v_successful_insert THEN
INSERT INTO my_table (value_type, value)
VALUES (p_value_type, p_value);
END IF;
RETURN v_successful_insert;
END;
$$
LANGUAGE plpgsql volatile;
The code is run on Postgresql 10.6.
You need to do a SELECT in the execute. For example:
DO $$
DECLARE
p_value TEXT := 'x';
p_value_constraint TEXT := '::int > 0';
result BOOLEAN;
BEGIN
BEGIN
EXECUTE 'SELECT $1' || p_value_constraint
INTO result
USING p_value;
EXCEPTION
WHEN INVALID_TEXT_REPRESENTATION THEN
result := FALSE;
END;
RAISE NOTICE '%', result;
END $$
Prints FALSE

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.