Save dynamic query to variable in postgres stored procedure - postgresql

I have the following postgres stored procedure:
CREATE OR REPLACE PROCEDURE
schema.MyProcedure()
AS $$
DECLARE
RowCount int;
BEGIN
SELECT cnt INTO RowCount
FROM (
SELECT COUNT(*) AS cnt
FROM MySchema.MyTable
) AS sub;
RAISE NOTICE 'RowCount: %', RowCount;
END;
$$
LANGUAGE plpgsql;
which "prints" out the row count of the static table MySchema.MyTable. How can it make it so I pass the Table and Schema name as an input.
eg:
CREATE OR REPLACE PROCEDURE
schema.MyProcedure(MySchema_In varchar, MyTable_In varchar)
AS $$
DECLARE
RowCount int;
BEGIN
SELECT cnt INTO RowCount
FROM (
SELECT COUNT(*) AS cnt
FROM || **MySchema_In** || . || **MyTable_In** ||
) AS sub;
RAISE NOTICE 'RowCount: %', RowCount;
END;
$$
LANGUAGE plpgsql;

You should use format() instead of concatenating the strings with || and then EXECUTE ... INTO to get the query's result, e.g.
CREATE OR REPLACE PROCEDURE MyProcedure(MySchema_In varchar, MyTable_In varchar)
AS $$
DECLARE RowCount int;
BEGIN
EXECUTE FORMAT('SELECT count(*) FROM %I.%I',$1,$2) INTO RowCount;
RAISE NOTICE 'RowCount: %', RowCount;
END;
$$
LANGUAGE plpgsql;

Related

Assigning query output to variable in postgres stored proc

I am trying to assign a variable the result of a query in a postgres stored procedure.
Here is what I am trying to run:
CREATE OR Replace PROCEDURE schema.MyProcedure()
AS $$
DECLARE
RowCount int = 100;
BEGIN
select cnt into RowCount
from (
Select count(*) as cnt
From schema.MyTable
) ;
RAISE NOTICE 'RowCount: %', RowCount;
END;
$$
LANGUAGE plpgsql;
schema.MyTable is just some arbitrary table name but the script is not displaying anything, not even the random value I assigned RowCount to (100).
What am I doing wrong?
Thanks
You need an alias for the subquery, for example : as sub
CREATE OR Replace PROCEDURE schema.MyProcedure()
AS $$
DECLARE
RowCount int = 100;
BEGIN
select cnt into RowCount
from (
Select count(*) as cnt
From schema.MyTable
) as sub ;
RAISE NOTICE 'RowCount: %', RowCount;
END;
$$
LANGUAGE plpgsql;
You can also assign any variable with a query result in parenthesis.
CREATE OR REPLACE PROCEDURE schema.my_procedure()
AS
$$
DECLARE
row_count BIGINT;
BEGIN
row_count = (SELECT COUNT(*) FROM schema.my_table);
RAISE NOTICE 'RowCount: %', row_count;
END;
$$ LANGUAGE plpgsql;
You should use BIGINT instead of INT.
And it's far better to write your code and table definition with snake_case style as possible.

Procedure to check count, store result and delete records

I want to create stored procedure to check count of query result. Then if count is > 0 to execute some query to delete records in other table. Below see what i got so far.
CREATE OR REPLACE PROCEDURE myprocedure(tableName VARCHAR, age INT, secondTable VARCHAR)
AS
$$
declare cnt := SELECT COUNT(*) FROM %tableName% WHERE ageID =%id%;
declare result;
BEGIN
EXECUTE cnt;
IF cnt >= 1 THEN
result := SELECT ID FROM %tableName% WHERE ageID =%id%
--remove records from secondTable
EXECUTE DELETE FROM %secondTable% WHERE ID IN (result)
END IF;
COMMIT;
END;
As documented in the manual you can't "reference" a variable with %tableName% and you certainly can not use a variable within a SQL statement for an identifier. You will need to use dynamic SQL.
You also got the DECLARE part completely wrong. You only write the keyword once, and you have to define a data type for the variables.
To create SQL strings that contain identifier, use format() and the %I placeholder to properly deal with identifiers that need quoting.
CREATE OR REPLACE PROCEDURE myprocedure(p_tablename VARCHAR, p_age INT, p_secondtable VARCHAR)
AS
$$
declare
l_sql text;
cnt integer;
BEGIN
l_sql := format('select count(*) from %I where ageid = :1', p_tablename);
EXECUTE l_sql
using p_age
into cnt;
IF cnt >= 1 THEN
l_sql := format('DELETE FROM %I WHERE ID IN (SELECT id FROM %I where ageid = :1)', p_secondtable, p_tablename);
EXECUTE l_sql using p_age;
END IF;
$$
language plpgsql;
But checking for the count before doing the delete is pretty pointless, you can simply that to a single DELETE statement:
CREATE OR REPLACE PROCEDURE myprocedure(p_tablename VARCHAR, p_age INT, p_secondtable VARCHAR)
AS
$$
declare
l_sql text;
cnt integer;
BEGIN
l_sql := format('DELETE FROM %I WHERE id IN (SELECT t.id FROM %I as t where t.ageid = :1)', p_secondtable, p_tablename);
EXECUTE l_sql using p_age;
END IF;
$$
language plpgsql;
Because the DELETE statement won't delete anything if the sub-select doesn't return any rows (which would be the case for cnt = 0). And you only need to query the first table once.

Postgresql: UPDATE before INSERT function

I have problem when create function for trigger. I want to UPDATE inserted value BEFORE INSERT data to DB.
My code look like this:
CREATE OR REPLACE FUNCTION test_func()
RETURNS TRIGGER AS
$$
DECLARE cnt INTEGER;
BEGIN
cnt := COUNT(*) FROM sample_tbl WHERE id = NEW.id AND created_date = NEW.created_date;
NEW.current_order := cnt + 1; // I want to set value of sample_tbl.current_order automatically
END
$$ LANGUAGE plpgsql;
CREATE TRIGGER test_trigger
BEFORE INSERT
ON test_tbl
FOR EACH ROW
EXECUTE PROCEDURE test_func();
I inserted data then IDE said:
control reached end of trigger procedure without RETURN
Where: PL/pgSQL function test_func()
The error says that you must return something from the Trigger ( either NEW or NULL )
There's no Trigger needed for this. A simple View using this select query will give you the required result
--create or replace view sample_view as
select t.id, t.created_date,
row_number() OVER ( partition by id,created_date order by id ) as current_order
FROM sample_tbl t;
This will exactly match the records if updated using a Trigger
CREATE OR REPLACE FUNCTION test_func()
RETURNS TRIGGER AS
$$
DECLARE cnt INTEGER;
BEGIN
select COUNT(*) INTO cnt FROM sample_tbl WHERE id = NEW.id
AND created_date = NEW.created_date;
NEW.current_order := cnt + 1;
RETURN NEW; --required
END
$$ LANGUAGE plpgsql;
Demo
Your trigger function is just missing RETURN NEW; statement:
CREATE OR REPLACE FUNCTION test_func()
RETURNS TRIGGER AS
$$
DECLARE cnt INTEGER;
BEGIN
cnt := COUNT(*) FROM sample_tbl WHERE id = NEW.id AND created_date = NEW.created_date;
NEW.current_order := cnt + 1;
RETURN NEW;
END
$$ LANGUAGE plpgsql;

plpgsql trigger - Passing a dynamic set of ids to an insert query (v9.6)

At a fundamental level this is what I want to accomplish.
CREATE OR REPLACE FUNCTION transform404activities() RETURNS TRIGGER LANGUAGE plpgsql AS $$
DECLARE
get404json text;
BEGIN
get404json := 'insert into public.events_404_normalized(event_name) select json_data->>''event_name'' from public.event_404 WHERE id IN(select id from public.event_404 WHERE created_at < (select now()) AND processed is NULL)';
EXECUTE format(get404json);
RETURN NEW;
END
$$;
This works but it's limited since I want to do extra steps with the id's in the WHERE IN select statement so I'm wanting to make the set/array a variable.
I started out with this:
CREATE OR REPLACE FUNCTION transform404activities() RETURNS TRIGGER LANGUAGE plpgsql AS $$
DECLARE
get404ids text;
result404ids int ARRAY;
get404json text;
mark404processed text;
BEGIN
get404ids := 'select id from public.event_404 WHERE created_at < (select now()) AND processed is NULL';
EXECUTE format(get404ids) INTO result404ids;
get404json := 'insert into public.events_404_normalized(event_name) select json_data->>''event_name'' from public.event_404 WHERE id = ANY (result404ids)';
EXECUTE format(get404json);
mark404processed := 'UPDATE public.event_404 SET processed = TRUE WHERE id IN(result404ids)';
RETURN NEW;
END
$$;
When the trigger is run the result gives an error of:
ERROR: malformed array literal: "51"
DETAIL: Array value must start with "{" or dimension information.
CONTEXT: PL/pgSQL function transform404activities() line 10 at EXECUTE
Which makes sense because it's not an array.
select id from public.event_404 WHERE created_at < (select now()) AND processed is NULL;
id
----
51
52
53
50
(4 rows)
However the moment I introduce array_agg the trigger wants to use the variable literally and not the contents of the array.
CREATE OR REPLACE FUNCTION transform404activities() RETURNS TRIGGER LANGUAGE plpgsql AS $$
DECLARE
get404ids text;
result404ids int ARRAY;
get404json text;
mark404processed text;
BEGIN
get404ids := 'select array_agg(id) from public.event_404 WHERE created_at < (select now()) AND processed is NULL';
EXECUTE format(get404ids) INTO result404ids;
get404json := 'insert into public.events_404_normalized(event_name) select json_data->>''event_name'' from public.event_404 WHERE id = ANY (result404ids)';
EXECUTE format(get404json);
mark404processed := 'UPDATE public.event_404 SET processed = TRUE WHERE id IN(result404ids)';
RETURN NEW;
END
$$;
The trigger results in this error:
ERROR: column "result404ids" does not exist
LINE 1: ...event_name' from public.event_404 WHERE id = ANY (result404i...
^
QUERY: insert into public.events_404_normalized(event_name) select json_data->>'event_name' from public.event_404 WHERE id = ANY (result404ids)
CONTEXT: PL/pgSQL function transform404activities() line 12 at EXECUTE
I'm expecting my variable to contain the following:
select array_agg(id) from public.event_404 WHERE created_at < (select now()) AND processed is NULL;
array_agg
---------------
{51,52,53,50}
I seem to be missing something basic but everything I've attempted to do results in the same "ERROR: column "result404ids" does not exist" error.
UPDATE
I rewrote my trigger and removed a lot of the cruft. This works as expected:
CREATE OR REPLACE FUNCTION transform404activities() RETURNS TRIGGER LANGUAGE plpgsql AS $$
DECLARE
result404ids text;
BEGIN
EXECUTE 'select array_agg(id) from public.event_404 WHERE created_at < (select now()) AND processed is NULL' INTO result404ids;
EXECUTE 'insert into public.events_404_normalized(event_name,language) select json_data->>''event_name'',json_data->>''language'' from event_404 WHERE id = ANY( '|| quote_literal(result404ids) ||')';
RETURN NEW;
END
$$;
UPDATE 2
Using Klin's suggestion my final result is:
CREATE OR REPLACE FUNCTION transform404activities() RETURNS TRIGGER LANGUAGE plpgsql AS $$
DECLARE
get404ids text;
result404ids int ARRAY;
get404json text;
BEGIN
EXECUTE 'select array_agg(id) from public.event_404 WHERE created_at < (select now()) AND processed is NULL' INTO result404ids;
get404json := 'insert into public.events_404_normalized(event_name,language) select json_data->>''event_name'', json_data->>''language'' from public.event_404 WHERE id = ANY ($1)';
execute get404json using result404ids;
RETURN NEW;
END
$$;
Use execute ... using ...:
...
get404json := 'insert into public.events_404_normalized(event_name) select json_data->>''event_name'' from public.event_404 WHERE id = ANY ($1)';
execute get404json using result404ids;
...
See Executing Dynamic Commands in the documentation.

Iterating over integer[] in plpgsql

How can I iterate over integer[] if I have:
operators_ids = string_to_array(operators_ids_g,',')::integer[];
I want iterate over operators_ids.
I can't do it in this way:
FOR oid IN operators_ids LOOP
and this:
FOR oid IN SELECT operators_ids LOOP
oid is integer;
You can iterate over an array like
DO
$body$
DECLARE your_array integer[] := '{1, 2, 3}'::integer[];
BEGIN
FOR i IN array_lower(your_array, 1) .. array_upper(your_array, 1)
LOOP
-- do something with your value
raise notice '%', your_array[i];
END LOOP;
END;
$body$
LANGUAGE plpgsql;
But the main question in my view is: why do you need to do this? There are chances you can solve your problem in better ways, for example:
DO
$body$
DECLARE i record;
BEGIN
FOR i IN (SELECT operators_id FROM your_table)
LOOP
-- do something with your value
raise notice '%', i.operators_id;
END LOOP;
END;
$body$
LANGUAGE plpgsql;
I think Dezso is right. You do not need to use looping the array using an index.
If you make a select statement grouping by person_id in combination with limit 1, you have the result set you wanted:
create or replace function statement_example(p_data text[]) returns int as $$
declare
rw event_log%rowtype;
begin
for rw in select * from "PRD".events_log where (event_type_id = 100 or event_type_id = 101) and person_id = any(operators_id::int[]) and plc_time < begin_date_g order by plc_time desc group by person_id limit 1 loop
raise notice 'interesting log: %', rw.field;
end loop;
return 1;
end;
$$ language plpgsql volatile;
That should perform much better.
If you still prefer looping an integer array and there are a lot of person_ids to look after, then might you consider using the flyweight design pattern:
create or replace function flyweight_example(p_data text[]) returns int as $$
declare
i_id int;
i_min int;
i_max int;
begin
i_min := array_lower(p_data,1);
i_max := array_upper(p_data,1);
for i_id in i_min .. i_max loop
raise notice 'interesting log: %',p_data[i_id];
end loop;
return 1;
end;
$$ language plpgsql volatile;