plpgsql how to insert results of query into table? - sql

I have a query A returning values integer, numeric, integer.
and a table B:
(id integer,
weight numeric,
price integer
)
the query returns many rows. I want to insert those rows directly to B. B doesn't have nor need a PK...
CREATE OR REPLACE FUNCTION func()
RETURNS void AS
$BODY$
begin
query A
insert to B?
continue function operation
end;
$BODY$
LANGUAGE plpgsql VOLATILE
I know it something like:
for row in query A
loop insert into B
but I can't find the proper syntax

You would do something like:
insert into b(id, weight, price)
select id, weight, price -- or whatever the column names are
from A;
The syntax of insert . . . select doesn't change because you are in a function.

Related

Dynamic query that uses CTE gets "syntax error at end of input"

I have a table that looks like this:
CREATE TABLE label (
hid UUID PRIMARY KEY DEFAULT UUID_GENERATE_V4(),
name TEXT NOT NULL UNIQUE
);
I want to create a function that takes a list of names and inserts multiple rows into the table, ignoring duplicate names, and returns an array of the IDs generated for the rows it inserted.
This works:
CREATE OR REPLACE FUNCTION insert_label(nms TEXT[])
RETURNS UUID[]
AS $$
DECLARE
ids UUID[];
BEGIN
CREATE TEMP TABLE tmp_names(name TEXT);
INSERT INTO tmp_names SELECT UNNEST(nms);
WITH new_names AS (
INSERT INTO label(name)
SELECT tn.name
FROM tmp_names tn
WHERE NOT EXISTS(SELECT 1 FROM label h WHERE h.name = tn.name)
RETURNING hid
)
SELECT ARRAY_AGG(hid) INTO ids
FROM new_names;
DROP TABLE tmp_names;
RETURN ids;
END;
$$ LANGUAGE PLPGSQL;
I have many tables with the exact same columns as the label table, so I would like to have a function that can insert into any of them. I'd like to create a dynamic query to do that. I tried that, but this does not work:
CREATE OR REPLACE FUNCTION insert_label(h_tbl REGCLASS, nms TEXT[])
RETURNS UUID[]
AS $$
DECLARE
ids UUID[];
query_str TEXT;
BEGIN
CREATE TEMP TABLE tmp_names(name TEXT);
INSERT INTO tmp_names SELECT UNNEST(nms);
query_str := FORMAT('WITH new_names AS ( INSERT INTO %1$I(name) SELECT tn.name FROM tmp_names tn WHERE NOT EXISTS(SELECT 1 FROM %1$I h WHERE h.name = tn.name) RETURNING hid)', h_tbl);
EXECUTE query_str;
SELECT ARRAY_AGG(hid) INTO ids FROM new_names;
DROP TABLE tmp_names;
RETURN ids;
END;
$$ LANGUAGE PLPGSQL;
This is the output I get when I run that function:
psql=# select insert_label('label', array['how', 'now', 'brown', 'cow']);
ERROR: syntax error at end of input
LINE 1: ...SELECT 1 FROM label h WHERE h.name = tn.name) RETURNING hid)
^
QUERY: WITH new_names AS ( INSERT INTO label(name) SELECT tn.name FROM tmp_names tn WHERE NOT EXISTS(SELECT 1 FROM label h WHERE h.name = tn.name) RETURNING hid)
CONTEXT: PL/pgSQL function insert_label(regclass,text[]) line 19 at EXECUTE
The query generated by the dynamic SQL looks like it should be exactly the same as the query from static SQL.
I got the function to work by changing the return value from an array of UUIDs to a table of UUIDs and not using CTE:
CREATE OR REPLACE FUNCTION insert_label(h_tbl REGCLASS, nms TEXT[])
RETURNS TABLE (hid UUID)
AS $$
DECLARE
query_str TEXT;
BEGIN
CREATE TEMP TABLE tmp_names(name TEXT);
INSERT INTO tmp_names SELECT UNNEST(nms);
query_str := FORMAT('INSERT INTO %1$I(name) SELECT tn.name FROM tmp_names tn WHERE NOT EXISTS(SELECT 1 FROM %1$I h WHERE h.name = tn.name) RETURNING hid', h_tbl);
RETURN QUERY EXECUTE query_str;
DROP TABLE tmp_names;
RETURN;
END;
$$ LANGUAGE PLPGSQL;
I don't know if one way is better than the other, returning an array of UUIDs or a table of UUIDs, but at least I got it to work one of those ways. Plus, possibly not using a CTE is more efficient, so it may be better to stick with the version that returns a table of UUIDs.
What I would like to know is why the dynamic query did not work when using a CTE. The query it produced looked like it should have worked.
If anyone can let me know what I did wrong, I would appreciate it.
... why the dynamic query did not work when using a CTE. The query it produced looked like it should have worked.
No, it was only the CTE without (required) outer query. (You had SELECT ARRAY_AGG(hid) INTO ids FROM new_names in the static version.)
There are more problems, but just use this query instead:
INSERT INTO label(name)
SELECT unnest(nms)
ON CONFLICT DO NOTHING
RETURNING hid;
label.name is defined UNIQUE NOT NULL, so this simple UPSERT can replace your function insert_label() completely.
It's much simpler and faster. It also defends against possible duplicates from within your input array that you didn't cover, yet. And it's safe under concurrent write load - as opposed to your original, which might run into race conditions. Related:
How to use RETURNING with ON CONFLICT in PostgreSQL?
I would just use the simple query and replace the table name.
But if you still want a dynamic function:
CREATE OR REPLACE FUNCTION insert_label(_tbl regclass, _nms text[])
RETURNS TABLE (hid uuid)
LANGUAGE plpgsql AS
$func$
BEGIN
RETURN QUERY EXECUTE format(
$$
INSERT INTO %s(name)
SELECT unnest($1)
ON CONFLICT DO NOTHING
RETURNING hid
$$, _tbl)
USING _nms;
END
$func$;
If you don't need an array as result, stick with the set (RETURNS TABLE ...). Simpler.
Pass values (_nms) to EXECUTE in a USING clause.
The tablename (_tbl) is type regclass, so the format specifier %I for format() would be wrong. Use %s instead. See:
Table name as a PostgreSQL function parameter

Why is RETURN QUERY returning a string instead of a TABLE

This MWE is NOT how you would typically solve this problem, however, it is as simple as I can explain the problem I am encountering. I am merely trying to point out 2 things
I am doing more than simply returning the contents of a Table
What is being returned is NOT being returned as a Table but a String
Supporting SQL Statements:
DROP DATABASE IF EXISTS test;
CREATE DATABASE test;
\c test
CREATE TABLE credit_card(
id BIGSERIAL PRIMARY KEY,
balance BIGINT
);
Functions:
CREATE FUNCTION get_credit_card(
p_id BIGINT
)
RETURNS TABLE(
id BIGINT,
balance BIGINT
)
AS $$
DECLARE
BEGIN
RETURN QUERY
SELECT
credit_card.id,
credit_card.balance
FROM
credit_card
WHERE
credit_card.id = p_id;
END $$ LAnguage 'plpgsql';
CREATE FUNCTION pay_with_card(
p_id BIGINT,
p_amount BIGINT
)
RETURNS TABLE(
id BIGINT,
balance BIGINT
)
AS $$
DECLARE
v_balance BIGINT;
BEGIN
SELECT
credit_card.balance
FROM
credit_card
INTO
v_balance
WHERE
credit_card.id = p_id;
IF v_balance < p_amount
THEN
RETURN;
END IF;
UPDATE
credit_card
SET
balance = credit_card.balance - p_amount;
RETURN QUERY
SELECT get_credit_card (p_id);
END $$ LAnguage 'plpgsql';
Populate Table and Call function:
INSERT INTO credit_card
(balance)
VALUES
(100);
SELECT
pay_with_card (1, 100);
Error:
DROP DATABASE
CREATE DATABASE
You are now connected to database "test" as user "postgres".
CREATE TABLE
CREATE FUNCTION
CREATE FUNCTION
INSERT 0 1
psql:test.sql:74: ERROR: structure of query does not match function result type
DETAIL: Returned type record does not match expected type bigint in column 1.
CONTEXT: PL/pgSQL function pay_with_card(bigint,bigint) line 24 at RETURN QUERY
It took me a long time to figure out that pay_with_card is returning a String, or what appears to be a String, instead of a TABLE(id BIGINT, balance BIGINT). With the Python psycopg2 library, the returned query is
[('(1,100)'),]
So my entire code is breaking because I can't get the values (unless I hack it and use string manipulation.
Question:
How can I fix it so that it returns the correct query like so
[(1,100),]
An alternative to the hint in horse_with_no_name's comment, you can replace
RETURN QUERY
SELECT get_credit_card (p_id);
with
RETURN QUERY SELECT (get_credit_card(p_id)).*;
You need some way of expanding the returned record back into its constituent fields. (I think horse’s SELECT * … has the same effect.)

PostgreSQL - How call a procedure with a varchar parameter

I am trying to create a SP in Postgresql 12:
CREATE OR REPLACE PROCEDURE trans_buy(
_name_client varchar(25),
_id_product smallint,
_mount smallint
)
LANGUAGE plpgsql
AS
$$
BEGIN
INSERT INTO invoices (cliente) VALUES(_name_client);
INSERT INTO invoices_details (id_invoice, id_product, mount) VALUES (1, _id_product, _mount, 100);
END
$$
But when I try to call this sp like so:
CALL trans_buy('james', 3, 10)
I receive this error message:
doesnt exists the stored procedure << trans_buy(unknown, integer, integer) >>
HINT: No procedure matches the name and types of arguments. It may be necessary to add explicit type conversion.
You probably need to explicitly cast the integer values to smallints:
call trans_buy('james', 3::smallint, 10::smallint);
I assume that the target columns in invoice_details are smallint as well. An alternative is to have the procedure accept ints, and cast at insert time:
CREATE OR REPLACE PROCEDURE trans_buy(
_name_client varchar(25),
_id_product int,
_mount int
) LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO invoices (cliente) VALUES(_name_client);
INSERT INTO invoices_details (id_invoice, id_product, mount) VALUES (1, _id_product::smallint, _mount::smallint);
END
$$
Note that your second insert had 4 values for 3 columns. I (attempted to) adjust that.
One should also highlight that you don't really need a subquery do this in Postgres. You can run multiple DML operations in a single query, using common-table-expressions:
with
-- CTE: query parameters
params(cliente, id_product, mount) as (values ('james', 3, 10)),
-- CTE: insert to invoices
inv as (insert into invoices (cliente) select cliente from params)
-- insert to invoice details
insert into invoice_details (id_invoice, id_product, mount)
select 1, id_product, mount from params

Is SELECT "faster" than function with nested INSERT?

I'm using a function that inserts a row to a table if it doesn't exist, then returns the id of the row.
Whenever I put the function inside a SELECT statement, with values that don't exist in the table yet, e.g.:
SELECT * FROM table WHERE id = function(123);
... it returns an empty row. However, running it again with the same values will return the row with the values I want to see.
Why does this happen? Is the INSERT running behind the SELECT speed? Or does PostgreSQL cache the table when it didn't exist, and at next run, it displays the result?
Here's a ready to use example of how this issue can occur:
CREATE TABLE IF NOT EXISTS test_table(
id INTEGER,
tvalue boolean
);
CREATE OR REPLACE FUNCTION test_function(user_id INTEGER)
RETURNS integer
LANGUAGE 'plpgsql'
AS $$
DECLARE
__user_id INTEGER;
BEGIN
EXECUTE format('SELECT * FROM test_table WHERE id = $1')
USING user_id
INTO __user_id;
IF __user_id IS NOT NULL THEN
RETURN __user_id;
ELSE
INSERT INTO test_table(id, tvalue)
VALUES (user_id, TRUE)
RETURNING id
INTO __user_id;
RETURN __user_id;
END IF;
END;
$$;
Call:
SELECT * FROM test_table WHERE id = test_function(4);
To reproduce the issue, pass any integer that doesn't exist in the table, yet.
The example is broken in multiple places.
No need for dynamic SQL with EXECUTE.
SELECT * in the function is wrong.
Your table definition should have a UNIQUE or PRIMARY KEY constraint on (id).
Most importantly, the final SELECT statement is bound to fail. Since the function is VOLATILE (has to be), it is evaluated once for every existing row in the table. Even if that worked, it would be a performance nightmare. But it does not. Like #user2864740 commented, there is also a problem with visibility. Postgres checks every existing row against the result of the function, which in turn adds 1 or more rows, and those rows are not yet in the snapshot the SELECT is operating on.
SELECT * FROM test_table WHERE id = test_function(4);
This would work (but see below!):
CREATE TABLE test_table (
id int PRIMARY KEY --!
, tvalue bool
);
CREATE OR REPLACE FUNCTION test_function(_user_id int)
RETURNS test_table LANGUAGE sql AS
$func$
WITH ins AS (
INSERT INTO test_table(id, tvalue)
VALUES (_user_id, TRUE)
ON CONFLICT DO NOTHING
RETURNING *
)
TABLE ins
UNION ALL
SELECT * FROM test_table WHERE id = _user_id
LIMIT 1
$func$;
And replace your SELECT with just:
SELECT * FROM test_function(1);
db<>fiddle here
Related:
Return a value if no record is found
How to use RETURNING with ON CONFLICT in PostgreSQL?
There is still a race condition for concurrent calls. If that can happen, consider:
Is SELECT or INSERT in a function prone to race conditions?

Use returned values from a query in the same procedure

I'm quite new to pgpsql. I have two tables - product and product_category - with a one to many relationship. I'm creating a procedure to insert a value into the category table if it doesn't exist and I would like to use the id for the category to insert a product.
I've tried using an alias for the first insert and then returning the id value so I can select it, but I get an error.
What's the right way for using returned in the same procedure values in pgpsql?
CREATE OR REPLACE PROCEDURE insert_product(name VARCHAR, category VARCHAR)
LANGUAGE plpgsql
as $$
BEGIN
WITH ins_category AS(
INSERT INTO product_category(name)
VALUES('laptops')
ON CONFLICT (name) DO NOTHING
RETURNING id
)
INSERT INTO product(name, category)
VALUES(name, ins_category.id); // HERE IS THE ERROR
COMMIT;
END;
$$
Use a SELECT instead of VALUES.
WITH ins_category
AS
(
...
)
INSERT INTO product (name,
category)
SELECT name,
ins_category.id
FROM ins_category;