Multiple ALTER TABLE ADD COLUMN in one SQL function call - sql

I came across some weird behaviour I'd like to understand.
I create a plpgsql function doing nothing except of ALTER TABLE ADD COLUMN. I call it 2 times on the same table:
A) In a single SELECT sentence
B) In a SQL function with same SELECT as in A)
Results are different: A) creates two columns, while B) creates only one column. Why?
Code:
CREATE FUNCTION add_text_column(table_name text, column_name text) RETURNS VOID
LANGUAGE plpgsql
AS $fff$
BEGIN
EXECUTE '
ALTER TABLE ' || table_name || '
ADD COLUMN ' || column_name || ' text;
';
END;
$fff$
;
-- this function is called only in B
CREATE FUNCTION add_many_text_columns(table_name text) RETURNS VOID
LANGUAGE SQL
AS $fff$
WITH
col_names (col_name) AS (
VALUES
( 'col_1' ),
( 'col_2' )
)
SELECT add_text_column(table_name, col_name)
FROM col_names
;
$fff$
;
-- A)
CREATE TABLE a (id integer);
WITH
col_names (col_name) AS (
VALUES
( 'col_1' ),
( 'col_2' )
)
SELECT add_text_column('a', col_name)
FROM col_names
;
SELECT * FROM a;
-- B)
CREATE TABLE b (id integer);
SELECT add_many_text_columns('b');
SELECT * FROM b;
Result:
CREATE FUNCTION
CREATE FUNCTION
CREATE TABLE
add_text_column
-----------------
(2 rows)
id | col_1 | col_2
----+-------+-------
(0 rows)
CREATE TABLE
add_many_text_columns
-----------------------
(1 row)
id | col_1
----+-------
(0 rows)
I'm using PostgreSQL 10.4. Please note that this is only a minimal working example, not the full functionality I need.

CREATE OR REPLACE FUNCTION g(i INTEGER)
RETURNS VOID AS $$
BEGIN
RAISE NOTICE 'g called with %', i;
END
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION t(i INTEGER)
RETURNS VOID AS $$
SELECT g(id)
FROM generate_series(1, i) id;
$$ LANGUAGE SQL;
What do you think happens when I run SELECT t(4)? The only statement printed from g() is g called with 1.
The reason for this is your add_many_text_columns function returns a single result (void). Because it's SQL and is simply returning the result of a SELECT statement, it seems to stop executing after getting the first result, which makes sense if you think of it - it can only return one result after all.
Now change the function to:
CREATE OR REPLACE FUNCTION t(i INTEGER)
RETURNS SETOF VOID AS $$
SELECT g(id)
FROM generate_series(1, i) id;
$$ LANGUAGE SQL;
And run SELECT t(4) again, and now this is printed:
g called with 1
g called with 2
g called with 3
g called with 4
Because the function now returns SETOF VOID, it doesn't stop after the first result and executes it fully.
So back to your functions, you could change your SQL function to return SETOF VOID, but it doesn't really make much sense - better I think to change it to plpgsql and have it do a PERFORM:
CREATE OR REPLACE FUNCTION t(i INTEGER)
RETURNS VOID AS $$
BEGIN
PERFORM g(id)
FROM generate_series(1, i) id;
END
$$ LANGUAGE plpgsql;
That will execute the statement fully and it still returns a single VOID.

eurotrash provided a good explanation.
Alternative solution 1
CREATE OR REPLACE FUNCTION t(i INTEGER)
RETURNS VOID AS
$func$
SELECT g(id)
FROM generate_series(1, i) id;
SELECT null::void;
$func$ LANGUAGE sql;
Because, quoting the manual:
SQL functions execute an arbitrary list of SQL statements, returning
the result of the last query in the list. In the simple (non-set)
case, the first row of the last query's result will be returned.
By adding a dummy SELECT at the end we avoid that Postgres stops after processing the the first row of the query with multiple rows.
Alternative solution 2
CREATE OR REPLACE FUNCTION t(i INTEGER)
RETURNS VOID AS
$func$
SELECT count(g(id))
FROM generate_series(1, i) id;
$func$ LANGUAGE sql;
By using an aggregate function, all underlying rows are processed in any case. The function returns bigint (that's what count() returns), so we get the number of rows as result.
Alternative solution 3
If you need to return void for some unknown reason, you can cast:
CREATE OR REPLACE FUNCTION t(i INTEGER)
RETURNS VOID AS
$func$
SELECT count(g(id))::text::void
FROM generate_series(1, i) id;
$func$ LANGUAGE sql;
The cast to text is a stepping stone because the cast from bigint to void is not defined.

Related

Postgres stored function return result of SELECT DISTINCT

I want to have an array of distinct integer values across my postgres table as the return value of a stored function.
The stored function currently looks like this
create or replace function get_unique_entries(id int)
returns table ("entry_id" int)
language plpgsql
as
$$
begin
return query
select distinct table.entry_id
from my_table
where x = id;
end;
$$;
When executing
select get_unique_entries(2);, I get the following error message:
structure of query does not match function result type
I tried different return types, but nothing worked for me.
Thanks in advance!
Hmm, can you give us a more complete picture of your scenario? I tried using your code and it seems to work (except I needed to replace table with my_table):
postgres=# create table my_table(x int, entry_id int, name text);
CREATE TABLE
postgres=# insert into my_table values(generate_series(1,100),generate_series(1,10),'foo');
INSERT 0 100
postgres=# create or replace function get_unique_entries(id int)
postgres-# returns table ("entry_id" int)
postgres-# language plpgsql
postgres-# as
postgres-# $$
postgres$# begin
postgres$# return query
postgres$# select distinct table.entry_id
postgres$# from my_table
postgres$# where x = id;
postgres$# end;
postgres$# $$;
ERROR: syntax error at or near "table"
LINE 8: select distinct table.entry_id
^
postgres=# create or replace function get_unique_entries(id int)
returns table ("entry_id" int)
language plpgsql
as
$$
begin
return query
select distinct my_table.entry_id
from my_table
where x = id;
end;
$$;
CREATE FUNCTION
postgres=# select get_unique_entries(2);
get_unique_entries
--------------------
2
(1 row)
postgres=#
While preparing the complete example I actually found it out myself.
As I am working with supabase, they display the datatype BIGINT as int8. I was trying to set this as return type. Setting the return type to BIGINT instead worked.
So in general check I would recommend myself and to others to check your column data types exactly.
The working example looks like this (as indicated by #richyen)
create or replace function get_unique_categories_for_platform(platformId int)
returns table ("category_fk" bigint)
language plpgsql
as
$$
begin
return query select distinct course.category_fk
from course
where platform_fk = platformId;
end;
$$;

grouping by parameters in postgres sql function and returning column name identical to the parameter

I have a query
select count(distinct id), usertype from mytable group by usertype;
which I want to wrap in a function where usertype is a parameter
create or replace function myfun(facet text)
RETURNS TABLE(total_accounts bigint, facet text)
LANGUAGE plpgsql
AS $$
select count(distinct id), facet from mytable group by facet;
$$;
select * from myfun('usertype');
so that output column name also depends on the parameter I send.
But it shows an error 'parameter facet is used more than once'.
how do I make the output identical to the original query when I pass 'usertype' as a parameter?
You can use a dynamic query in a plpgsql function :
create or replace function myfun(OUT total_accounts bigint, INOUT facet text)
RETURNS setof record LANGUAGE plpgsql
AS $$
BEGIN
RETURN QUERY EXECUTE '
select count(distinct id),' || quote_ident(facet) || ' from mytable group by ' || quote_ident(facet) ;
END ;
$$;
select * from myfun('usertype');
test result in dbfiddle

call an sql function by name returned by select

I would like to find out how to call a function the name of which is returned from a select query. So let's say, I have a select query:
SELECT function_name FROM functions WHERE id=1;
Now let's say, the returned functions name is fce1
and now I want to execute:
SELECT fce1(parameters);
Now my initial idea would be:
SELECT CONCAT(SELECT function_name FROM functions WHERE id=1;, "(params)");
I am quite certain that the idea is wrong. But I was trying to figure that out some time ago and I remember that at least MS SQL was able to achieve my goal and also POSTGRESQL. Anyway, neither do I remember or am I able to find how to do it. Ideas would be appreciated.
DECLARE #func NVARCHAR(50);
SELECT #func = function_name FROM functions WHERE id=1;
EXEC ('select ' + #func + '()')
Assuming all functions return the same data type, you could build a wrapper function in Postgres that you pass the ID of the function to be called:
create or replace function call_func(p_id integer)
returns integer
as
$$
declare
l_result integer;
l_name text;
l_params text;
begin
select function_name, parameters
into l_name, l_params
from functions
where id = p_id;
execute 'select '||l_name||'('||l_params||')'
into l_result;
return l_result;
end;
$$
language plpgsql;
Note: the above is just an example.
It's wide open to SQL injection and does not do any error checking or sanitizing the parameters! But it might point you into the right direction.
Assume you have the functions:
create or replace function foo(p_arg_1 integer, p_arg_2 integer)
returns integer
as
$$
select p_arg_1 + p_arg_2;
$$
language sql;
create or replace function bar(p_value integer)
returns integer
as
$$
select p_value * 4;
$$
language sql;
And the functions table looks like this:
id | function_name | parameters
---+---------------+-----------
1 | fce | 42
2 | foo | 1,2
You can then do
select call_func(2);
or
select call_func(1);
But again: this will only work if all functions return the same result. e.g. scalar functions returning a single value, or set returning functions returning the same table definition.

How to declare returning type for function, returning unnamed table

I want to create a function, that returns a table with 2 columns:
i integer -- or bigint?
arr integer[] -- array of integer
What should I write instead of ??? in this function:
CREATE OR REPLACE FUNCTION test()
RETURNS ???
LANGUAGE plpgsql
AS $$
BEGIN
RETURN QUERY (
SELECT i, (ARRAY[11,22,33])[i]
FROM generate_series(
1,
array_upper(ARRAY[11,22,33],1)
) i
);
END;
$$;
see the answer for this question,
example of function return table:
CREATE OR REPLACE FUNCTION foo(a int)
RETURNS TABLE(b int, c int) AS $$
BEGIN
RETURN QUERY SELECT i, i+1 FROM generate_series(1, a) g(i);
END;
$$ LANGUAGE plpgsql;
You don't need plpgsql for this. A simple SQL function would do the job:
CREATE OR REPLACE FUNCTION test(_arr anyarray)
RETURNS TABLE (idx int, elem anyelement)
$func$
SELECT i, _arr[i] FROM generate_subscripts(_arr, 1) i
$func$ LANGUAGE sql AS
Call:
SELECT * FROM test(ARRAY[11,22,33]::int[]); -- Cast to declare type for literals
The polymorphic parameter anyarray works for arrays of any base type.
How to write a function that returns text or integer values?
Use generate_subscripts() to simplify the task.
More about returning from a function:
How to return result of a SELECT inside a function in PostgreSQL?
Postgres 9.4
There is a shiny new trick in upcoming Postgres 9.4: WITH ORDINALITY. Details in this related answer:
PostgreSQL unnest() with element number
Simplify to (no additional function):
SELECT * FROM unnest(ARRAY[11,22,33]::int[]) WITH ORDINALITY AS x (elem, idx)

Return a query from a function?

I am using PostgreSQL 8.4 and I want to create a function that returns a query with many rows.
The following function does not work:
create function get_names(varchar) returns setof record AS $$
declare
tname alias for $1;
res setof record;
begin
select * into res from mytable where name = tname;
return res;
end;
$$ LANGUAGE plpgsql;
The type record only allows single row.
How to return an entire query? I want to use functions as query templates.
CREATE OR REPLACE FUNCTION get_names(_tname varchar)
RETURNS TABLE (col_a integer, col_b text) AS
$func$
BEGIN
RETURN QUERY
SELECT t.col_a, t.col_b -- must match RETURNS TABLE
FROM mytable t
WHERE t.name = _tname;
END
$func$ LANGUAGE plpgsql;
Call like this:
SELECT * FROM get_names('name')
Major points:
Use RETURNS TABLE, so you don't have to provide a list of column names with every call.
Use RETURN QUERY, much simpler.
Table-qualify column names to avoid naming conflicts with identically named OUT parameters (including columns declared with RETURNS TABLE).
Use a named variable instead of ALIAS. Simpler, doing the same, and it's the preferred way.
A simple function like this could also be written in LANGUAGE sql:
CREATE OR REPLACE FUNCTION get_names(_tname varchar)
RETURNS TABLE (col_a integer, col_b text) AS
$func$
SELECT t.col_a, t.col_b --, more columns - must match RETURNS above
FROM mytable t
WHERE t.name = $1;
$func$ LANGUAGE sql;