PostgreSQL Function Returning Table or SETOF - sql

Quick background: very new to PostgreSQL, came from SQL Server. I am working on converting stored procedures from SQL Server to PostgreSQL. I have read Postgres 9.3 documentation and looked through numerous examples and questions but still cannot find a solution.
I have this select statement that I execute weekly to return new values
select distinct id, null as charges
, generaldescription as chargedescription, amount as paidamount
from mytable
where id not in (select id from myothertable)
What can I do to turn this into a function where I can just right click and execute on a weekly basis. Eventually I will automate this using a program another developer built. So it will execute with no user involvement and send results in a spreadsheet to the user. Not sure if that last part matters to how the function is written.
This is one of my many failed attempts:
CREATE FUNCTION newids
RETURNS TABLE (id VARCHAR, charges NUMERIC
, chargedescription VARCHAR, paidamount NUMERIC) AS Results
Begin
SELECT DISTINCT id, NULL AS charges
, generaldescription AS chargedescription, amount AS paidamount
FROM mytable
WHERE id NOT IN (SELECT id FROM myothertable)
END;
$$ LANGUAGE plpgsql;
Also I am using Navicat and the error is:
function result type must be specified

You can use PL/pgSQL here and it may even be the better choice.
Difference between language sql and language plpgsql in PostgreSQL functions
But you need to fix a syntax error, match your dollar-quoting, add an explicit cast (NULL::numeric) and use RETURN QUERY:
CREATE FUNCTION newids_plpgsql()
RETURNS TABLE (
id varchar
, charges numeric
, chargedescription varchar
, paidamount numeric
) AS -- Results -- remove this
$func$
BEGIN
RETURN QUERY
SELECT ...;
END;
$func$ LANGUAGE plpgsql;
Or use a simple SQL function:
CREATE FUNCTION newids_sql()
RETURNS TABLE (
id varchar
, charges numeric
, chargedescription varchar
, paidamount numeric
) AS
$func$
SELECT ...;
$func$ LANGUAGE sql;
Either way, the SELECT statement can be more efficient:
SELECT DISTINCT t.id, NULL::numeric AS charges
, t.generaldescription AS chargedescription, t.amount AS paidamount
FROM mytable t
LEFT JOIN myothertable m ON m.id = t.id
WHERE m.id IS NULL;
Select rows which are not present in other table
Of course, all data types must match. I can't tell, table definition is missing.
And are you sure you need DISTINCT?

Related

Create a function that accepts a string and returns multiple rows

I'm being required to create a function that transforms a single column's value based on the user's input. I need some help on the syntax for doing so.
Here is the query I'm currently performing to get the rows:
SELECT payment_id, rental_id, amount FROM payment
some pseudocode on what I'm trying to do:
function getReport(String currencyType){
if(currencyType == 'EUR'){
Multiply the values in the amounts column by 1.16 and append Euros to it
Return all the rows in the table
}else if(currencyType == 'RMB'){
Multiple the values in the amounts column by 6.44 and append RMB to it
Return all the rows in the table
}else{
Do nothing because the default column values are in USD
Return all the rows in the table
}
}
I've been trying to create one but I'm struggling with the syntax.
Does not work:
CREATE OR REPLACE FUNCTION get_data(currency_type text) RETURNS TABLE payment_info AS $$
CASE currency_type
WHEN 'EUR' THEN
SELECT payment_id, rental_id, amount * 1.16 FROM payment;
WHEN 'RMB' THEN
SELECT payment_id, rental_id, amount * 6.44 FROM payment;
WHEN 'USD' THEN
SELECT payment_id, rental_id, amount FROM payment;
$$ LANGUAGE SQL;
Could someone please help me with the syntax to creating this function?
Something like this
CREATE OR REPLACE FUNCTION get_data(currency_type text)
RETURNS TABLE ( payment_id int, rental_id int, amount numeric(5,2) )
language plpgsql
as $$
begin
return query
SELECT b.payment_id, b.rental_id,
case
when currency_type = 'EUR' then b.amount * 1.16
when currency_type = 'RMB' then b.amount * 6.44
when currency_type = 'USD' then b.amount
end as amount
FROM payment b;
end;$$
It does return in the form of a table if you use
select * from get_data('EUR');
Here a demo
demo in db<>fiddle
Postgres 14 or later
In Postgres 14 or later, I'd suggest the new standard SQL syntax:
CREATE OR REPLACE FUNCTION get_data(_currency_type text DEFAULT 'USD')
RETURNS TABLE (payment_id int, rental_id int, amount numeric(5,2))
STABLE PARALLEL SAFE
BEGIN ATOMIC
SELECT p.payment_id, p.rental_id
, CASE _currency_type
WHEN 'USD' THEN p.amount
WHEN 'EUR' THEN p.amount * 1.16
WHEN 'RMB' THEN p.amount * 6.44
FROM payment p;
END;
See:
What does BEGIN ATOMIC ... END mean in a Postgresql SQL function / procedure?
Most of the below still applies ...
Postgres 13 (or any version)
CREATE OR REPLACE FUNCTION get_data(_currency_type text DEFAULT 'USD')
RETURNS TABLE (payment_id int, rental_id int, amount numeric(5,2))
LANGUAGE sql STABLE PARALLEL SAFE AS
$func$
SELECT p.payment_id, p.rental_id
, CASE _currency_type
WHEN 'USD' THEN p.amount
WHEN 'EUR' THEN p.amount * 1.16
WHEN 'RMB' THEN p.amount * 6.44
-- ELSE 1/0 END -- ??? this purposely raises an exception
FROM payment p;
$func$;
Stick with LANGUAGE sql (like in your original attempt). There is no need for LANGUAGE plpgsql for the simple function - unless you want to add a custom error message or error handling for invalid input ..
See:
Difference between language sql and language plpgsql in PostgreSQL functions
Use a simpler switched CASE (like in your original attempt).
Display column name with max value between several columns
Simplify nested case when statement
Provide an explicit ELSE branch. (SQL CASE defaults to NULL if ELSE is not spelled out.)
If payment_info, featured in your original attempt, is an existing row type matching your desired return type, use a simple RETURNS SETOF payment_info instead of RETURNS TABLE(...).
It's good style to table-qualify columns with possibly ambiguous names like demonstrated. (It's never a bad idea in any case.)
But it's a requirement in a LANGUAGE plpgsql function with RETURNS TABLE ...) implicitly declaring OUT parameters of the same name as table columns. That would raise an exception like:
ERROR: column reference "payment_id" is ambiguous
See:
INSERT INTO ... RETURNING - ambiguous column reference
How to return result of a SELECT inside a function in PostgreSQL?
Also: numeric(5,2)? That raises an exception for amount > 999.99. Looks like a loaded foot-gun. Just use numeric or something like numeric(20,2) for the rounding effect.
About STABLE:
How do IMMUTABLE, STABLE and VOLATILE keywords effect behaviour of function?
About PARALLEL SAFE:
When to mark functions as PARALLEL RESTRICTED vs PARALLEL SAFE?
Finally, as has been cleared up already, to decompose result rows, call with SELECT * FROM:
SELECT * FROM get_data('USD');
See:
Simple PostgreSQL function to return rows
PostgreSQL: ERROR: 42601: a column definition list is required for functions returning "record"
I added an default value for the input parameter: DEFAULT 'USD'. That's convenient, but totally optional. It documents what's most commonly expected and allows a short call without explicit parameter:
SELECT * FROM get_data();

Table variable join equivalent in Oracle

I'm given quite a huge table My_Table and a user-defined collection Picture_Arr as an input usually having 5-10 rows declared as:
TYPE Picture_Rec IS RECORD (
seq_no NUMBER,
task_seq NUMBER);
TYPE Picture_Arr IS TABLE OF Picture_Rec;
In MS SQL I would normally write something like:
DECLARE #Picture_Arr TABLE (seq_no INT, task_seq INT)
SELECT M.*
FROM My_Table M
INNER JOIN #Picture_Arr A
ON M.seq_no = A.seq_no AND M.task_seq = A.task_seq
But I can't get my head around how to re-write the same code in Oracle as Picture_Arr is not a table. As some tutorials state that I could've looped through My_Table and compare keys, but is it efficient in Oracle or is there another way of doing that?
Perhaps this is what you are looking for. It is a bit complicated to understand what is the desired output, and whether the data of the record is stored somewhere or not
create type Picture_Rec as object(
seq_no NUMBER,
task_seq NUMBER);
)
/
create type Picture_Tab as table of Picture_Rec
/
create or replace function get_picture_list
return Picture_Tab
is
l_pic Picture_Tab;
begin
select Picture_Rec ( seqno, taskseq )
bulk collect into l_pic
from your_table; -- the table you have these records
return l_pic;
end;
/
Then you run
SELECT M.*
FROM My_Table M
JOIN TABLE ( get_picture_list() ) p
ON M.seq_no = p.seq_no AND M.task_seq = p.task_seq

Function to select existing value or insert new row

I'm new to working with PL/pgSQL, and I'm attempting to create a function that will either find the ID of an existing row, or will insert a new row if it is not found, and return the new ID.
The query contained in the function below works fine on its own, and the function gets created fine. However, when I try to run it, I get an error stating "ERROR: column reference "id" is ambiguous". Can anybody identify my problem, or suggest a more appropriate way to do this?
create or replace function sp_get_insert_company(
in company_name varchar(100)
)
returns table (id int)
as $$
begin
with s as (
select
id
from
companies
where name = company_name
),
i as (
insert into companies (name)
select company_name
where not exists (select 1 from s)
returning id
)
select id
from i
union all
select id
from s;
end;
$$ language plpgsql;
This is how I call the function:
select sp_get_insert_company('TEST')
And this is the error that I get:
SQL Error [42702]: ERROR: column reference "id" is ambiguous
Detail: It could refer to either a PL/pgSQL variable or a table column.
Where: PL/pgSQL function sp_get_insert_company(character varying) line 3 at SQL statement
As the messages says, id is in there twice. Once in the queries, once in the table definition of the return type. Somehow this clashes.
Try qualifying the column expressions, everywhere.
...
with s as (
select
companies.id
from
companies
where name = company_name
),
i as (
insert into companies (name)
select company_name
where not exists (select 1 from s)
returning companies.id
)
select i.id
from i
union all
select s.id
from s;
...
By qualifying the column expression the DBMS does no longer confuse id of a table with the id in the return type definition.
The next problem will be, that your SELECT has no target. It will tell you to do a PERFORM instead. But I assume you want to return the results. Change the body to
...
RETURN QUERY (
with s as (
select
companies.id
from
companies
where name = company_name
),
i as (
insert into companies (name)
select company_name
where not exists (select 1 from s)
returning companies.id
)
select i.id
from i
union all
select s.id
from s);
...
to do so.
In the function you display there is no need for returns table (id int). It's supposed to always return exactly one integer ID. Simplify to RETURNS int. This also makes ERROR: column reference "id" is ambiguous go away, since we implicitly removed the OUT parameter id (visible in the whole function block).
How to return result of a SELECT inside a function in PostgreSQL?
There is also no need for LANGUAGE plpgsql. Could simply be LANGUAGE sql, then you wouldn't need to add RETURNS QUERY, either. So:
CREATE OR REPLACE FUNCTION sp_get_insert_company(_company_name text)
RETURNS int AS
$func$
WITH s as (
select c.id -- still good style to table-qualify all columns
from companies c
where c.name = _company_name
),
i as (
insert into companies (name)
select _company_name
where not exists (select 1 from s)
returning id
)
select s.id from s
union all
select i.id from i
LIMIT 1; -- to optimize performance
$func$ LANGUAGE sql;
Except that it still suffers from concurrency issues. Find a proper solution for your undisclosed version of Postgres in this closely related answer:
Is SELECT or INSERT in a function prone to race conditions?

Working with where clauses inside views

CREATE VIEW summary AS
SELECT sales.inventory_id,
sum(sales.value) AS value,
count(sales.*) AS sales_n,
FROM promotion
JOIN sales USING (promotion_id)
GROUP BY sales.inventory_id
;
This is a simplified query of a problem that I am dealing with
However, I would like to be able to add a WHERE clause into the view. Something like this WHERE promotion.created_at > ?
QUESTION:
How should I modify the view so that I can do something like this:
SELECT *
FROM summary
WHERE promotion.created_at > [timestamp]
I am currently not able to do this because there is a GROUP BY on inventory_id so promotion.created_at is not captured
Use an SQL function taking the timestamp as parameter instead:
CREATE FUNCTION summary(_created_at timestamp)
RETURNS TABLE (inventory_id int, value bigint, sales_n int)
AS
$func$
SELECT s.inventory_id
, sum(s.value) -- AS value -- alias not visible outside function
, count(*) -- AS sales_n
FROM promotion p
JOIN sales s USING (promotion_id)
WHERE p.created_at > $1
GROUP BY s.inventory_id
$func$ LANGUAGE sql STABLE;
Call:
SELECT * FROM summary('2014-06-01 10:00');
I shortened the syntax with table aliases.
count(*) does the same as count(s.*) here.
Adapt RETURNS TABLE(...) to your actual data types.

RETURN QUERY-Record in PostgreSQL

I am trying to write a PostgreSQL function that inserts data in the database and then receives some data and returns it.
Here is the code:
CREATE OR REPLACE FUNCTION newTask(projectid api.objects.projectid%TYPE, predecessortaskid api.objects.predecessortaskid%TYPE, creatoruserid api.objects.creatoruserid%TYPE, title api.objects.title%TYPE, description api.objects.description%TYPE, deadline api.objects.deadline%TYPE, creationdate api.objects.creationdate%TYPE, issingletask api.tasks.issingletask%TYPE)
RETURNS SETOF api.v_task AS
$$
DECLARE
v_objectid api.objects.objectid%TYPE;
BEGIN
INSERT INTO api.objects(objectid, projectid, predecessortaskid, creatoruserid, title, description, deadline, creationdate) VALUES (DEFAULT, projectid, predecessortaskid, creatoruserid, title, description, deadline, creationdate)
RETURNING objectid INTO v_objectid;
INSERT INTO api.tasks(objectid, issingletask) VALUES (v_objectid, issingletask);
RETURN QUERY (SELECT * FROM api.v_task WHERE objectid = v_objectid);
END;
$$ LANGUAGE plpgsql;
objects and tasks are both tables and v_task is a view, which is a join of the two. The reason why I return data that I just inserted is that there are some triggers doing work on it.
So far so good. I use RETURNS SETOF api.v_task as my return type and RETURN QUERY (...) and therefore expect the result to look like a SELECT from v_task (same columns with same data types). However, what really happens is (output from pgAdmin, same result from my node.js-application):
SELECT newTask( CAST(NULL AS integer), CAST(NULL AS integer), 1, varchar 'a',varchar 'a', cast(NOW() as timestamp(0) without time zone), cast(NOW() as timestamp(0) without time zone), true);
newtask
api.v_task
--------
"(27,,,1,a,a,"2012-03-19 12:15:50","2012-03-19 12:15:50","2012-03-19 12:15:49.629997",,t)"
Instead of several column the output is forced into one, separated by commas.
As I am already using a special record type I can't use the AS keyword to specify the fields of my output.
Calling a table function
To retrieve individual columns from a function returning multiple columns (effectively a composite type or row type), call it with:
SELECT * FROM func();
If you want to, you can also just SELECT some columns and not others. Think of such a function (also called table function) like of a table:
SELECT objectid, projectid, title FROM func();
Alternative here: plain SQL
If you use PostgreSQL 9.1 or later you might be interested in this variant. I use a writable CTE to chain the INSERTs.
One might be tempted to add the final SELECT as another module to the CTE, but that does not work in this case, because the newly inserted values are not visible in the view within the same CTE. So I left that as a separate command - without brackets around the SELECT:
CREATE OR REPLACE FUNCTION new_task (
_projectid api.objects.projectid%TYPE
,_predecessortaskid api.objects.predecessortaskid%TYPE
,_creatoruserid api.objects.creatoruserid%TYPE
,_title api.objects.title%TYPE
,_description api.objects.description%TYPE
,_deadline api.objects.deadline%TYPE
,_creationdate api.objects.creationdate%TYPE
,_issingletask api.tasks.issingletask%TYPE)
RETURNS SETOF api.v_task AS
$func$
DECLARE
_objectid api.objects.objectid%TYPE;
BEGIN
RETURN QUERY
WITH x AS (
INSERT INTO api.objects
( projectid, predecessortaskid, creatoruserid, title
, description, deadline, creationdate)
VALUES (_projectid, _predecessortaskid, _creatoruserid, _title
, _description, _deadline, _creationdate)
RETURNING objectid
)
INSERT INTO api.tasks
(objectid, issingletask)
SELECT x.objectid, _issingletask
FROM x
RETURNING objectid INTO _objectid;
RETURN QUERY
SELECT * FROM api.v_task WHERE objectid = _objectid;
END
$func$ LANGUAGE plpgsql;