Cursor retrieves a deleted row in table created within plpgsql function - postgresql

Within a plpgsql function I create a table and use a cursor to access its rows. While at first row, I delete a following one and surprisingly (to me at least) the cursor fetches it. When repeating within the same function, it works as I expected it so.
However, if the table pre-exists and is not created within the function, the deleted row is never fetched.
What am I missing?
DECLARE
curs1 refcursor;
rec record;
BEGIN
CREATE TABLE test as select generate_series(1,5,1) test_id;
OPEN curs1 FOR SELECT * FROM test ORDER BY test_id;
LOOP
FETCH curs1 INTO rec;
EXIT WHEN NOT FOUND;
RAISE NOTICE 'ROW:%',rec.test_id;
IF rec.test_id=1 THEN
DELETE FROM TEST WHERE test_id=3;
END IF;
END LOOP;
CLOSE curs1;
RAISE NOTICE 'AGAIN';
--just repeating without deleting
OPEN curs1 FOR SELECT * FROM test ORDER BY test_id;
LOOP
FETCH curs1 INTO rec;
EXIT WHEN NOT FOUND;
RAISE NOTICE 'ROW:%',rec.test_id;
END LOOP;
CLOSE curs1;
Output is:
NOTICE: ROW:1
NOTICE: ROW:2
NOTICE: ROW:3
NOTICE: ROW:4
NOTICE: ROW:5
NOTICE: AGAIN
NOTICE: ROW:1
NOTICE: ROW:2
NOTICE: ROW:4
NOTICE: ROW:5

The reason is that Postgres cursors are "insensitive" by default. The documentation:
The SQL standard says that it is implementation-dependent whether
cursors are sensitive to concurrent updates of the underlying data by
default. In PostgreSQL, cursors are insensitive by default, and can be
made sensitive by specifying FOR UPDATE. Other products may work differently.
Bold emphasis mine.
So try the same with using the FOR UPDATE clause:
DO
$$
DECLARE
curs1 refcursor;
rec record;
BEGIN
CREATE TABLE test AS SELECT generate_series(1,5) test_id;
OPEN curs1 FOR SELECT * FROM test ORDER BY test_id FOR UPDATE;
DELETE FROM test WHERE test_id = 3;
LOOP
FETCH curs1 INTO rec;
EXIT WHEN NOT FOUND;
RAISE NOTICE 'ROW:%',rec.test_id;
END LOOP;
CLOSE curs1;
END
$$
And you get:
NOTICE: ROW:1
NOTICE: ROW:2
NOTICE: ROW:4
NOTICE: ROW:5
Row 3 is not visible any more.

Related

POSTGRESQL Database Triggers anf functions

I am trying to create a trigger in postgres that if you are trying to add an existing id within a customers table it will raise an error and won't allow you to do it.
Trigger Code:
create trigger id_check()
before insert on customers
for each row execute procedure duplicates()
Function:
create or replace function duplicates()
returns trigger as $BODY$
begin
if exists(select 1 from customers where id = new.id)
then raise notice 'cannot have a duplicate id'
return new;
end;
$BODY$ LANGUAGE plpgsql;
I keep getting errors either and I'm not understanding what's wrong? Any help would be great.
As indicated a unique constraint is the appropriate method to properly handle this. But if you insist on your trigger then then you must cleanup the syntax errors: need semi-colon after raise statement, and end if at conclusion of IF. Also, learn now to format your code.
create or replace function duplicates()
returns trigger as $BODY$
begin
if exists(select 1 from customers where id = new.id)
then raise notice 'cannot have a duplicate id'; --- added ;
end if; --- added line
return new;
end;
$BODY$ LANGUAGE plpgsql;

prepared statement "fooplan" already exists

Running the following SQL statement:
create or replace procedure test_insert_prepare2(howmany int)
language plpgsql
as $$
declare counter integer:=0;
declare note int;
begin
prepare fooplan(int) as
insert into procedure_testing_table values($1, $2);
select count(*) into strict note from procedure_testing_table;
while counter < howmany loop
counter := counter +1;
note := note +1;
execute fooplan(note,'testing_prepare');
end loop;
end;
$$
always shows an error:
ERROR: prepared statement "fooplan" already exists
How could I use prepare statement correctly in a while loop?
Prepared statements live until the end of the database session or until you explicitly deallocate them, so the second invocation of the function will cause this error.
But it is not necessary to use a prepared statement here, because PL/pgSQL automatically caches execution plans. So use a plain INSERT inside the loop, it will be just as efficient.

Postgres: syntax error when trying to create multiple stored procedures from a single file

I am using Postgres version 13.1. I want to have a single file where I could store all stored procedures and create them in one shot. But when I put multiple stored procedures in a single file, I get the following error. What am I missing?
In my test.sql file I have the following content:
create or replace procedure tmp1(
)
language plpgsql as
$$
declare
l_count integer;
begin
select 1 into l_count;
raise info 'count: %', l_count;
end;
$$
create or replace procedure tmp2(
)
language plpgsql as
$$
declare
l_count integer;
begin
select 1 into l_count;
raise info 'count: %', l_count;
end;
$$
If I only have the first procedure it gets created and I can call it successfully. But the moment I have the second identical procedure with a different name, it gives me an error as follows (when run from psql):
psql=> \i test.sql
psql:test.sql:23: ERROR: syntax error at or near "create"
LINE 12: create or replace procedure tmp2(
OK - right after posting I tried putting a ";" after the end of first procedure and it works

PL/pgSQL "for loop" + select basic example ("hello world")

I've been using Postgres for a while, but I'm totally new to PL/pgSQL.
I'm struggling to get a basic for loop to work.
This works fine:
-- Without SELECT
DO $$
BEGIN
FOR counter IN 1..6 BY 2 LOOP
RAISE NOTICE 'Counter: %', counter;
END LOOP;
END; $$;
But what I really want is to iterate through the result of a SELECT query.
I keep running into this error:
Error in query: ERROR: loop variable of loop over rows must be a record or row variable or list of scalar variables
Sounds pretty obscure to me and googling did not help.
There's a table from my own data I want to use (I was hoping to use a SELECT * FROM mytable WHERE ‹whatever›), but I realize I can't even get the for loop to work with simpler data.
Take this:
-- with a SELECT
DO $$
BEGIN
RAISE NOTICE 'Get ready to be amazed…';
FOR target IN SELECT * FROM generate_series(1,2) LOOP
RAISE NOTICE 'hello'
END LOOP;
END; $$
This generates the error above too. I'd like to get a simple thing printed to get the hang of the loop syntax, something like:
hello 1
hello 2
What am I doing wrong?
The iterator must be declared
DO $$
DECLARE
target record;
BEGIN
RAISE NOTICE 'Get ready to be amazed…';
FOR target IN SELECT * FROM generate_series(1,2) LOOP
RAISE NOTICE 'hello';
END LOOP;
END; $$;
NOTICE: Get ready to be amazed…
NOTICE: hello
NOTICE: hello

PostgreSQL: Checking for NEW and OLD in a function for a trigger

I want to create a trigger which counts rows and updates a field in an other table. My current solution works for INSERT statements but failes when I DELETE a row.
My current function:
CREATE OR REPLACE FUNCTION update_table_count()
RETURNS trigger AS
$$
DECLARE updatecount INT;
BEGIN
Select count(*) into updatecount
From source_table
Where id = new.id;
Update dest_table set count=updatecount
Where id = new.id;
RETURN NEW;
END;
$$
LANGUAGE 'plpgsql';
The trigger is a pretty basic one, looking like.
CREATE TRIGGER count_trigger
AFTER INSERT OR DELETE
ON source_table
FOR EACH ROW
EXECUTE PROCEDURE update_table_count();
When I excute a DELETE statement the following error occurs:
ERROR: record "new" is not assigned yet
DETAIL: The tuple structure of a not-yet-assigned record is indeterminate.
I know one solution could be to create just one set of trigger and function for the DELETE and one for the INSERT statement. But I want to do it a bit more elegant and want to know, if there is a solution to check if NEW or OLD is present in the current context and just implement an IF ELSE block. But I dont know how to check for this context sensitive items.
Thanks for your help
The usual approach to make a trigger function do different things depending on how the trigger was fired is to check the trigger operation through TG_OP
CREATE OR REPLACE FUNCTION update_table_count()
RETURNS trigger AS
$$
DECLARE
updatecount INT;
BEGIN
if tg_op = 'UPDATE' then
select count(*) into updatecount from source_table where id = new.id;
update dest_table set count=updatecount where id = new.id;
elsif tg_op = 'DELETE' then
... do something else
end if;
RETURN NEW;
END;
$$
LANGUAGE plpgsql;
Unrelated, but: the language name is an identifier. Do not quote it using single quotes.
From PostgreSQL's documentation:
NEW
Data type RECORD; variable holding the new database row for INSERT/UPDATE operations in row-level triggers. This variable is null in statement-level triggers and for DELETE operations.
OLD
Data type RECORD; variable holding the old database row for UPDATE/DELETE operations in row-level triggers. This variable is null in statement-level triggers and for INSERT operations.
So, for example, if NEW is NULL, then the trigger was invoked on DELETE.