Functions with variable number of input parameters - sql

I'm creating a stored procedure (function) in a PostgreSQL DB, which updates a table depending on its input. In order to create a variable number of parameter function, I'm creating an extra input parameter called mode, which I use to control which parameters I use on the update query.
CREATE OR REPLACE FUNCTION update_site(
mode integer,
name character varying,
city character varying,
telephone integer,
)
RETURNS integer AS
$$
BEGIN
IF mode = 0 THEN
BEGIN
UPDATE "Sites" SET
("City","Telephone") = (city,telephone)
WHERE "SiteName" = name;
RETURN 1;
EXCEPTION WHEN others THEN
RAISE NOTICE 'Error on site update: %, %',SQLERRM,SQLSTATE;
RETURN 0;
END;
ELSIF mode = 1 THEN
BEGIN
UPDATE "Sites" SET "City" = city
WHERE "SiteName" = name;
RETURN 1;
EXCEPTION WHEN others THEN
RAISE NOTICE 'Error on site update: %, %',SQLERRM,SQLSTATE;
RETURN 0;
END;
ELSIF mode = 2 THEN
BEGIN
UPDATE "Sites" SET "Telephone" = telephone
WHERE "SiteName" = name;
RETURN 1;
EXCEPTION WHEN others THEN
RAISE NOTICE 'Error on site update: %, %',SQLERRM,SQLSTATE;
RETURN 0;
END;
ELSE
RAISE NOTICE 'Error on site update: %, %',SQLERRM,SQLSTATE;
RETURN 0;
END IF;
END;
$$ LANGUAGE plpgsql;
What would be best? To create a function update_site(<all the columns of table>) and a separate function update_site(id integer, <varchar column to update>), or use the mode in one function to define the difference? Which option is more efficient? One unique function or different ones for each purpose?

Advanced features like VARIADIC or even polymorphic input types and dynamic SQL are very powerful. The last chapter in this answer provides an advanced example:
Refactor a PL/pgSQL function to return the output of various SELECT queries
But for a simple case like yours, you can just use default values for function parameters. It all depends on exact requirements.
If the columns in question are all defined NOT NULL, this would probably be simpler and faster:
CREATE OR REPLACE FUNCTION update_site(_name text -- always required
, _city text DEFAULT NULL
, _telephone integer DEFAULT NULL)
RETURNS integer AS
$func$
BEGIN
IF _city IS NULL AND _telephone IS NULL THEN
RAISE WARNING 'At least one value to update required!';
RETURN; -- nothing to update
END IF;
UPDATE "Sites"
SET "City" = COALESCE(_city, "City")
, "Telephone" = COALESCE(_telephone, "Telephone")
WHERE "SiteName" = _name;
END
$func$ LANGUAGE plpgsql;
Read about default values in the manual!
To avoid naming conflicts between parameters and column names I make it a habit to prefix input parameters with _. That's a matter of taste and style.
The first parameter name has no default, since it is required at all times.
Other parameters can be omitted.
At least one is required, or a WARNING is raised and nothing else happens.
The UPDATE will only change columns for given parameters.
Can easily be expanded for N parameters.
Function call
Since Postgres 9.5:
The simple way is with positional notation for parameters. This only allows to omit the rightmost parameter(s):
SELECT update_site('foo', 'New York'); -- no telephone
Named notation allows to omit any parameter that has a default value:
SELECT update_site(name => 'foo', _telephone => 123); -- no city
Both can be combined in mixed notation:
SELECT update_site('foo', _telephone => 123); -- still no city
In Postgres 9.4 or older, := was used for assignment in the call:
SELECT update_site(name := 'foo', _telephone := 123);
SELECT update_site('foo', _telephone := 123);
Still valid in Postgres 12 for backward compatibility, but rather use the modern notation.

There are a few things you'll want to look into:
Dynamically building the SQL using the format function and its %I and %L specifiers, then executing it with EXECUTE ... USING; and
Using VARIADIC parameters to take variable numbers of arguments to the function, with the caveat that they must all be the same data type.

Related

How to write function for optional parameters in postgresql?

My requirement is write optional parameters to a function.Parameters are optional sometimes i will add or i will not pass parameters to function.Can anyone help me how to write function.
I am writing like
select *
from test
where field3 in ('value1','value2')
and ($1 is null or field1 = $1)
and ($2 is null or field2 = $2)
and ($3 is null or field3 = $3);
i am passing parameters to Query,But my output is not expected.when i pass all three parameters my output is correct,otherwise it is not expected output.
You can define optional parameters by supplying a default value.
create function foo(p_one integer default null,
p_two integer default 42,
p_three varchar default 'foo')
returns text
as
$$
begin
return format('p_one=%s, p_two=%s, p_three=%s', p_one, p_two, p_three);
end;
$$
language plpgsql;
You can "leave out" parameters from the end, so foo(), foo(1) or foo(1,2) are valid. If you want to only supply a parameter that is not the first you have to use the syntax that specifies the parameter names.
select foo();
returns: p_one=, p_two=42, p_three=foo
select foo(1);
returns: p_one=1, p_two=42, p_three=foo
select foo(p_three => 'bar')
returns: p_one=, p_two=42, p_three=bar
Apart of the VARIADIC option pointed by #a_horse_with_no_name, which is only a syntax sugar for passing an array with any number of elements of the same type, you can't define a function with optional parameters because, in postgres, functions are identified not only by its name but also by its arguments and the types of them.
That is: create function foo (int) [...] and create function foo (varchar) [...] will create different functions.
Which is called when you execute, for example, select foo(bar) depends on bar data type itself. That is: if it is an integer, you will call the first one and if it is varchar, then second one will be called.
More than that: if you execute, for example, select foo(now()), then a function not exists exception will be triggered.
So, as I said, you can't implement functions with variable arguments, but you can implement multiple functions with the same name and distinct argument (an/or type) sets returning the same data type.
If you (obviously) doesn't want to implement the function twice, the only thing you need to do is to implement a "master" function with all possible parameters and the others (which have fewer parameters) only calling the "master" one with default values for the non received parameters.
As an option, I got a function i tested with Navicat App:
CREATE OR REPLACE FUNCTION "public"."for_loop_through_query"(sponsor_name varchar default 'Save the Children')
It generates me this. (Note: Please look at the parameter difference)
CREATE OR REPLACE FUNCTION "public"."for_loop_through_query"("sponsor_name" varchar='Save the Children'::character varying)
CREATE OR REPLACE FUNCTION "public"."for_loop_through_query"("sponsor_name" varchar='Save the Children'::character varying)
RETURNS "pg_catalog"."void" AS $BODY$
DECLARE
rec RECORD;
BEGIN
FOR rec IN SELECT
companies."name" AS org_name,
"sponsors"."name" AS sponsor_name
FROM
"donor_companies"
JOIN "sponsors"
ON "donor_companies"."donor_id" = "sponsors"."id"
JOIN companies
ON "donor_companies"."organization_id" = companies."id"
WHERE
"public"."sponsors"."name" = sponsor_name
LOOP
RAISE NOTICE '%', rec.org_name;
END LOOP;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;

How to derive column name in UPDATE from variable in a PL/pgSQL trigger function?

I have a simple trigger function in PostgreSQL 9.4:
BEGIN
IF (TG_OP = 'UPDATE') THEN
UPDATE relation
SET child_name = new.name
WHERE table_reference_1 = new.id;
END IF;
RETURN NULL;
END;
Is it possible to replace table_reference_1 (which is column name) with variable? I want to do something like:
BEGIN
IF (TG_OP = 'UPDATE') THEN
UPDATE relation
SET child_name = new.name
WHERE TG_TABLE_NAME = new.id;
END IF;
RETURN NULL;
END;
WHERE TG_TABLE_NAME = new.id is supposed to mean:
"new.id is equal to the value of the column, whose name is equal to the parent table's name".
Plain SQL does not accept variables for identifiers. I see two options for your trigger function:
1. CASE expression
For a couple of known alternatives (and an optional catch-all).
UPDATE relation r
SET child_name = NEW.name
WHERE CASE TG_TABLE_NAME -- "switched case"
WHEN 'possible_column1' -- value!
THEN r.possible_column1 = NEW.id -- identifier!
WHEN 'possible_column2'
THEN r.possible_column2 = NEW.id
-- etc.
-- ELSE r.default_column = NEW.id
-- or no ELSE ...
END;
No ELSE means the expression evaluates to NULL if no option matches. And only TRUE qualifies in a WHERE clause.
2. Dynamic SQL
For any number of alternatives or for alternatives unknown at the time of coding.
EXECUTE format('
UPDATE relation
SET child_name = $1
WHERE %I = $2'
, TG_TABLE_NAME -- being used as column name
USING NEW.name, NEW.id;
Notes
If the column name doesn't actually exist, this raises an exception. Your transaction is rolled back unless you trap it.
PL/pgSQL operates with prepared statements. The query plan for option 1 can be reused within the same session if Postgres finds that re-planning does not generate better plans than a generic plan. Option 2 is planned every time. This may be irrelevant / a disadvantage / an actual advantage, depending on your use case ...
Difference between language sql and language plpgsql in PostgreSQL functions
Always make sure that dynamic SQL is safe against SQL injection (by maliciously crafted table names in this case). I defend against it with format() using %I.
Related answers with more explanation:
Table name as a PostgreSQL function parameter
INSERT with dynamic table name in trigger function
Update multiple columns in a trigger function in plpgsql
Difference between language sql and language plpgsql in PostgreSQL functions

Elegant way of handling PostgreSQL exceptions?

In PostgreSQL, I would like to create a safe-wrapping mechanism which returns empty result if an exception occurs. Consider the following:
SELECT * FROM myschema.mytable;
I could do the safe-wrapping in the client application:
try {
result = execute_query('SELECT value FROM myschema.mytable').fetchall();
}
catch(pg_exception) {
result = []
}
But could I do such a thing in SQL directly? I would like to make the following code work, but it seems like it should by put into DO $$ ... $$ block and here I'm getting lost.
BEGIN
SELECT * FROM myschema.mytable;
EXCEPTION WHEN others THEN
SELECT unnest(ARRAY[]::TEXT[])
END
Exception handling in PL/pgSQL
PL/pgSQL code is always wrapped into a BEGIN ... END block. That can be inside the body of a DO statement or a function. Blocks can be nested inside - but they cannot exist outside, don't confuse it with plain SQL.
Each block can optionally contain an EXCEPTION clause for handling exceptions, but functions that need to trap exceptions are more expensive, so it's best to avoid exceptions a priori. Postgres needs to prepare for the possibility of rolling back to a point in the transaction before the exception happened, similar to an SQL SAVEPOINT. The manual:
A block containing an EXCEPTION clause is significantly more
expensive to enter and exit than a block without one. Therefore, don't
use EXCEPTION without need.
Example:
Is SELECT or INSERT in a function prone to race conditions?
How to avoid an exception in the example
A DO statement can't return anything. Create a function that takes table and schema name as parameters and returns whatever you want:
CREATE OR REPLACE FUNCTION f_tbl_value(_tbl text, _schema text = 'public')
RETURNS TABLE (value text)
LANGUAGE plpgsql AS
$func$
DECLARE
_t regclass := to_regclass(_schema || '.' || _tbl);
BEGIN
IF _t IS NULL THEN
value := ''; RETURN NEXT; -- return single empty string
ELSE
RETURN QUERY EXECUTE
'SELECT value FROM ' || _t; -- return set of values
END IF;
END
$func$;
Call:
SELECT * FROM f_tbl_value('my_table');
Or:
SELECT * FROM f_tbl_value('my_table', 'my_schema');
Assuming you want a set of rows with a single text column or an empty string if the table does not exist.
Also assuming that a column value exists if the given table exists. You could test for that, too, but you didn't ask for that.
Both input parameters are only case sensitive if double-quoted. Just like identifiers are handled in SQL statements.
The schema name defaults to 'public' in my example. Adapt to your needs. You could even ignore the schema completely and default to the current search_path.
to_regclass() is new in Postgres 9.4. For older versions substitute:
IF EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = _schema
AND table_name = _tbl
) THEN ...
This is actually more accurate, because it tests exactly what you need. More options and detailed explanation:
Table name as a PostgreSQL function parameter
Always defend against SQL injection when working with dynamic SQL! The cast to regclass does the trick here. More details:
How to check if a table exists in a given schema
If you are selecting only one column then the COALESCE() function should be able to do the trick for you
SELECT COALESCE( value, '{}'::text[] ) FROM myschema.mytable
If you require more rows you may require to create a function with types.

Use DEFAULT value if empty string

How can I tell Postgres that a column with an empty string should use the DEFAULT value?
Using CHECK doesn't seem to work here.
A check constraint will only prevent putting such a value into the column. It will not magically replace the supplied value with the default value.
In order to silently replace an empty string (or null) with the value 'active' you will need a trigger:
create or replace function check_default()
returns trigger
as
$body$
begin
if (new.status is null or new.status = '') then
new.status = 'active';
end if;
return new;
end;
$body$
language plpgsql;
The above trigger would catch stuff like this:
insert into foo (id, active)
values (42,'');
update foo
set active = ''
where id = 42;
You could even extend that logic to also replace whitespace only values (' ') with the desired value.
Although it is possible to retrieve the default value dynamically in the trigger (to avoid having the same constant in two places) I would not do that for performance reasons.

Set a default return value for a Postgres function

I have the following function in Postgres:
CREATE OR REPLACE FUNCTION point_total(user_id integer, gametime date)
RETURNS bigint AS
$BODY$
SELECT sum(points) AS result
FROM picks
WHERE user_id = $1
AND picks.gametime > $2
AND points IS NOT NULL;
$BODY$
LANGUAGE sql VOLATILE;
It works correctly, but when a user starts out and has no points, it very reasonably returns NULL. How can I modify it so that it returns 0 instead.
Changing the body of the function to that below results in an "ERROR: syntax error at or near "IF".
SELECT sum(points) AS result
FROM picks
WHERE user_id = $1
AND picks.gametime > $2
AND points IS NOT NULL;
IF result IS NULL
SELECT 0 AS result;
END;
You need to change the language from sqlto plpgsql if you want to use the procedural features of PL/pgSQL. The function body changes, too.
Be aware that all parameter names are visible in the function body, including all levels of SQL statements. If you create a naming conflict, you may need to table-qualify column names like this: table.col, to avoid confusion. Since you refer to function parameters by positional reference ($n) anyway, I just removed parameter names to make it work.
Finally, THEN was missing in the IF statement - the immediate cause of the error message.
One could use COALESCE to substitute for NULL values. But that only works if there is at least one resulting row. COALESCE can't fix "no row" it can only replace actual NULL values.
There are several ways to cover all NULL cases. In plpgsql functions:
CREATE OR REPLACE FUNCTION point_total(integer, date, OUT result bigint)
RETURNS bigint AS
$func$
BEGIN
SELECT sum(p.points) -- COALESCE would make sense ...
INTO result
FROM picks p
WHERE p.user_id = $1
AND p.gametime > $2
AND p.points IS NOT NULL; -- ... if NULL values were not ruled out
IF NOT FOUND THEN -- If no row was found ...
result := 0; -- ... set to 0 explicitly
END IF;
END
$func$ LANGUAGE plpgsql;
Or you can enclose the whole query in a COALESCE expression in an outer SELECT. "No row" from the inner SELECT results in a NULL in the expression. Work as plain SQL, or you can wrap it in an sql function:
CREATE OR REPLACE FUNCTION point_total(integer, date)
RETURNS bigint AS
$func$
SELECT COALESCE(
(SELECT sum(p.points)
FROM picks p
WHERE p.user_id = $1
AND p.gametime > $2
-- AND p.points IS NOT NULL -- redundant here
), 0)
$func$ LANGUAGE sql;
Related answer:
How to display a default value when no match found in a query?
Concerning naming conflicts
One problem was the naming conflict most likely. There have been major changes in version 9.0. I quote the release notes:
E.8.2.5. PL/pgSQL
PL/pgSQL now throws an error if a variable name conflicts with a
column name used in a query (Tom Lane)
Later versions have refined the behavior. In obvious spots the right alternative is picked automatically. Reduces the potential for conflicts, but it's still there. The advice still applies in Postgres 9.3.