how to use argument as table name in a postgresql SQL function - sql

I've got a function to return relevant position of an array matching given value like below:
CREATE OR REPLACE FUNCTION get_index(needle ANYELEMENT, haystack ANYARRAY)
RETURNS TABLE (i_position int)
AS $$
SELECT
i+i_step AS i_position
FROM (VALUES(1),(2)) steps(i_step),
generate_series(array_lower($2,1), array_upper($2,1)) AS i
WHERE $2[i] = $1
$$ LANGUAGE SQL STABLE;
Instead of passing a single value to the function, I want to pass a table name, as one column of the table would be used to do the value comparison (WHERE $2[i] = $1 ), instead of a single value passed to the function. However, it doesn't seem like the function support SQL using argument as table name.
I'm wondering if there's alternative. I'd like to use SQL function instead of PLPGSQL, for the sake of performance. As our table is huge.
I'd like to achieve something like below:
CREATE OR REPLACE FUNCTION get_index(tbl ANYELEMENT, haystack ANYARRAY)
RETURNS TABLE (i_position int)
AS $$
SELECT
i+i_step AS i_position
FROM (VALUES(1),(2)) steps(i_step),
generate_series(array_lower($2,1), array_upper($2,1)) AS i,
$1
WHERE $2[i] = $1.col1
$$ LANGUAGE SQL STABLE;

It is not possible in SQL function - It doesn't support dynamic sql. You have to use PLpgSQL - EXECUTE statement.
SQL function is faster than PLpgSQL only when inlining is successful. When it isn't successful, and SQL function should be evaluated, then PLpgSQL should not be slower. When body of SQL function contains SRF functions like generate_series, then inlining is not effective.

Related

POSTGRESQL How to return multiple rows from SQL function?

I'm new to databases and SQL and i've been messing around with functions on POSTGRESQL and I made a simple function to select all names from a table.
But the function, when called, returns just a single row as opposed to all rows being returned when i use SELECT c_name FROM customers; instead of the code shown below:
CREATE OR REPLACE FUNCTION get_cust_names()
RETURNS varchar AS
$body$
SELECT c_name FROM customers;
$body$
LANGUAGE SQL
Function call
SELECT get_cust_names()
this returns just a SINGLE row and this isnt code that i will use in a project etc. I'm just curious as to why postgresql behaves this way.
You need to declare the function as returns table(..) or returns setof
CREATE OR REPLACE FUNCTION get_cust_names()
RETURNS table(cust_name varchar)
AS
$body$
SELECT c_name
FROM customers;
$body$
LANGUAGE SQL;
Then use it like a table:
select *
from get_cust_names();

"ERROR: query has no destination for result data"

I am trying to create a function like this:
I tried changing the return type to int or text etc. to see if the code works outside of that, but it doesn't. I am a beginner in PostgreSQL so please don't be harsh if I missed something obvious.
create or replace function date_select(i INT) returns void as
$$
begin
select * from dwh_stg.stg_dce_gift where gift_id = i;
end
$$ language plpgsql
select date_select(16940)
SQL Error [42601]:
ERROR: query has no destination for result data
Hint: If you want to discard the results of a SELECT, use PERFORM instead.
Where: PL/pgSQL function date_select(integer) line 3 at SQL statement
If you want to return something, you need to define the function to return something (not void)
Apparently you want to return multiple rows from the table stg_dec_gift, for that you need to define the function as returns setof dwh_stg.stg_dce_gift. For a simple function encapsulating a query there is not need to use PL/pgSQL a plain SQL function will do just fine:
create or replace function date_select(i INT)
returns setof dwh_stg.stg_dce_gift --<< here
as
$$
select *
from dwh_stg.stg_dce_gift
where gift_id = i;
$$
stable
language sql;
Then use it in the FROM part:
select *
from date_select(16940);
Online example: https://rextester.com/WYDCE44062

Function parameter anyelement, PostgreSQL bug?

I do not see the bug in this implementation:
CREATE FUNCTION foo(anyelement) RETURNS SETOF int AS $f$
SELECT id FROM unnest(array[1,2,3]) t(id)
WHERE CASE WHEN (pg_typeof($1)::text)='integer' THEN $1::int>2 ELSE true END
$f$ LANGUAGE SQL IMMUTABLE;
SELECT * FROM foo(123); -- OK!
SELECT * FROM foo('test'::text); -- BUG
Is this some kind of PostgreSQL bug or a non-documented restriction on anyelement datatype?
Interesting: when isolated the CASE clause works fine:
CREATE FUNCTION bar(anyelement) RETURNS boolean AS $f$
SELECT CASE WHEN (pg_typeof($1)::text)='integer' THEN $1::int>2;
$f$ LANGUAGE SQL IMMUTABLE;
SELECT bar('test'::text), bar(123), bar(1); -- works fine!
Your problem is due to how SQL statements are planned. SQL is very rigid about data types. Postgres functions provide some flexibility with the polymorphic pseudo-type ANYELEMENT, but the SQL statement is still planned with the given types statically.
While the expression $1::int>2 is never executed if $1 is not an integer (you could avoid division by zero this way), this cannot save you from the syntax error that arises at the earlier stage of planning the query.
You can still do something with the function you have. Use an untyped string literal:
CREATE OR REPLACE FUNCTION foo(anyelement)
RETURNS SETOF int AS
$func$
SELECT id FROM unnest(array[1,2,3]) id
WHERE CASE WHEN pg_typeof($1) = 'integer'::regtype
THEN $1 > '2' -- use a string literal!
ELSE true END
$func$ LANGUAGE sql IMMUTABLE;
This at least works for all character and numeric data types. The string literal is coerced to the provided data type. But it will still fail for other data types where '2' is not valid.
It's remarkable that your second example does not trigger the syntax error. It emerged from my tests on Postgres 9.5 that the syntax error is triggered if the function is not IMMUTABLE or for set-returning functions (RETURNS SETOF ... instead of RETURNS boolean) that are called in the FROM list: SELECT * FROM foo() instead of SELECT foo(). It would seem that query planning is handled differently for simple IMMUTABLE functions which can be inlined.
Aside, use:
pg_typeof($1) = 'integer'::regtype
instead of:
(pg_typeof($1)::text)='integer'
That's generally better. It's always better to cast the constant once instead of the computed value every time. And this works for known aliases of the type name as well.
PostgreSQL syntax error in parameterized query on "date $1"
It's definitely related to SQL planner/optimizer. Since the function is declared as IMMUTABLE, the optimizer tries to pre-evaluate the query parts. For some reason, it evaluates the expression $1::int>2 even if you call the function with text parameter.
If you change your foo function to VOLATILE it will work fine, because the query optimizer will not try to optimize/pre-evalute it.
But why bar function works fine even if it's IMMUTABLE? I guess the optimizer decides not to pre-evalute it as it does not use expressions in loops. What I mean is that $1::int>2 is evaluated only once, whereas in foo function it's evaluated multiple times.
Seems like there are some differences how SQL planner works for SQL and PLPGSQL language. The same function in PLPGSQL works fine.
CREATE FUNCTION foo2(anyelement) RETURNS SETOF int AS $f$
DECLARE
i INTEGER;
BEGIN
FOR i IN SELECT id FROM unnest(array[1,2,3]) t(id)
WHERE
CASE WHEN pg_typeof($1) = 'integer'::regtype
THEN $1::int > 2
ELSE true END
LOOP
RETURN NEXT i;
END LOOP;
END;
$f$ LANGUAGE plpgsql IMMUTABLE;
SELECT * FROM foo2('test'::text); -- works fine

Passing multiple values in single parameter

Let's say I have this function:
CREATE OR REPLACE FUNCTION test_function(character varaying)
RETURNS integer AS
$BODY$
DECLARE
some_integer integer;
begin
Select column2 from test_table where column1 in ($1) into some_integer;
end;
Return some_integer;
$BODY$
LANGUAGE plpgsql VOLATILE COST 100;
And I want to call it like this:
Select * from test_function ('data1', 'data2','data3');
Of course, it cannot be done this way, because Postgres tries to find function with this name and three parameter which doesn't exists.
I tried to put quotes around commas but in that case parameter is interpreted wrong: data1', 'data2','data3, like one string.
Is there a way to put multiple values in parameter so IN clause can recognized it?
(Your function wouldn't be created. RETURN after END is syntactical nonsense.)
A function with a VARIADIC parameter does exactly what you ask for:
CREATE OR REPLACE FUNCTION test_function(date, date, VARIADIC varchar[])
RETURNS SETOF integer
LANGUAGE sql AS
$func$
SELECT col1
FROM test_table
WHERE col3 > $1
AND col4 < $2
AND col2 = ANY($3)
$func$;
db<>fiddle here - demo with additional parameters
Old sqlfiddle
Call (as desired):
SELECT * FROM test_function('data1', 'data2', 'data3');
Using a simple SQL function, plpgsql is not required for the simple example. But VARIADIC works for plpgsql functions, too.
Using RETURNS SETOF integer since this can obviously return multiple rows.
Details:
Pass multiple values in single parameter
Return rows matching elements of input array in plpgsql function
VARIADIC parameter must be the last input parameter
Return rows matching elements of input array in plpgsql function

Error when calling a function

I have one function that returns all employee IDs
Function definition is like this:
CREATE OR REPLACE FUNCTION tmp()
RETURNS setof record AS
$func$
begin
select emp_id from employee_master;
end;
$func$
LANGUAGE plpgsql;
But when i call this function using
select * from tmp() as abc(emp_id text);
It gives error like
ERROR: query has no destination for result data
HINT: If you want to discard the results of a SELECT, use PERFORM instead.
CONTEXT: PL/pgSQL function "tmp" line 3 at SQL statement
Please give solution :)
If you want to return a rowset from a PL/PgSQL function you must use RETURN - in this case, probably RETURN QUERY:
RETURN QUERY SELECT emp_id FROM employee_master;
I don't see the point of having this in a PL/PgSQL function at all, though.
Make the function a plain SQL one as in:
...
LANGUAGE SQL;
It is much more practical to declare the actual type of the column instead of the unwieldy record type. Assuming emp_id to be integer, a simple SQL function could look like this:
CREATE OR REPLACE FUNCTION tmp()
RETURNS SETOF integer AS
$func$
SELECT emp_id FROM employee_master
$func$ LANGUAGE sql;
However, the error message in your comment does not match the given question. Depending on your actual requirements, you would adjust the RETURN type.