How do I integrate a FOR loop into my dynamic queries? - postgresql

I am trying to run a dynamic query. I have a static query that works :
CREATE OR REPLACE
FUNCTION update_points_in_polygon(
)
RETURNS trigger LANGUAGE plpgsql AS $function$
-- Rule for clarity : one filter per function. Can choose three options : point count unit count and label concat
-- the one rule will be applied to all 3 options if they are selected
DECLARE tmprow record;
BEGIN
FOR tmprow IN
SELECT c.objectid, count(a.*) point_count
FROM sandbox.simple_address a
JOIN sandbox.polygon_address_units c
ON st_intersects(a.wkb_geometry, c.wkb_geometry)
WHERE st_intersects(c.wkb_geometry, NEW.wkb_geometry)
GROUP BY c.objectid -- tmp TABLE fetchin nb OF addresses IN a polygon WHERE polygon interects point
LOOP
UPDATE sandbox.polygon_address_units
SET address_count = tmprow.point_count
WHERE objectid = tmprow.objectid; -- UPDATE point_count COLUMN OF TABLES fetched IN FIRST SELECT
END LOOP;
RETURN NEW;
END;
$function$;
When I try to replace the loop like this :
EXECUTE 'FOR tmprow IN
SELECT c.objectid, count(a.*) point_count
FROM sandbox.simple_address a
JOIN sandbox.polygon_address_units c
ON st_intersects(a.wkb_geometry, c.wkb_geometry)
WHERE st_intersects(c.wkb_geometry, NEW.wkb_geometry)
GROUP BY c.objectid
LOOP
UPDATE sandbox.polygon_address_units
SET address_count = tmprow.point_count
WHERE objectid = tmprow.objectid;
END LOOP;';
I get the following message :
SQL Error [42601]: ERROR: syntax error at or near "FOR" Where:
PL/pgSQL function update_points_in_polygon() line 7 at EXECUTE
The aim is to be able to define table_names and columns dynamically. I've also tried to make two execute statements and keep the LOOP in plpgsql instead of plain SQL that's run with EXECUTE:
CREATE OR REPLACE
FUNCTION update_points_in_polygon(
)
RETURNS trigger LANGUAGE plpgsql AS $function$
-- Rule for clarity : one filter per function. Can choose three options : point count unit count and label concat
-- the one rule will be applied to all 3 options if they are selected
DECLARE tables_schema varchar;
DECLARE polygon_table_name varchar;
DECLARE polygon_point_count_column_name varchar;
DECLARE point_table_name varchar;
DECLARE tmprow record;
BEGIN
tables_schema = TG_ARGV[0]; -- frontier_ftth
polygon_table_name = TG_ARGV[1]; -- fdh
polygon_point_count_column_name = TG_ARGV[2]; -- point_count
point_table_name = TG_ARGV[4];
FOR tmprow IN
EXECUTE format('SELECT c.objectid, count(a.*) point_count
FROM %I.%I a
JOIN %I.%I c
ON st_intersects(a.wkb_geometry, c.wkb_geometry)
WHERE st_intersects(c.wkb_geometry, st_geomFromText(''%s'', 4326))
GROUP BY c.objectid',
tables_schema, point_table_name, tables_schema, polygon_table_name,
st_astext(st_geomfromewkb(NEW.wkb_geometry)))
LOOP
EXECUTE format('UPDATE %I.%I c1
SET %I = tmprow.point_count
WHERE c1.objectid = tmprow.objectid',
tables_schema, polygon_table_name, polygon_point_count_column_name);
END LOOP;
RETURN NEW;
END;
$function$
With this version of the function, I get the following error :
SQL Error [22004]: ERROR: null values cannot be formatted as an SQL identifier
Where: PL/pgSQL function update_points_in_polygon() line 17 at FOR over EXECUTE statement
This is a barebones version of my setup :
CREATE TABLE sandbox.simple_address (
objectid serial4 NOT NULL,
wkb_geometry geometry(point, 4326) NULL,
);
CREATE TABLE sandbox.polygon_address_units (
objectid serial4 NOT NULL,
address_count int4 NULL,
wkb_geometry geometry(multipolygon, 4326) NULL,
);
CREATE TRIGGER onallactions AFTER INSERT
OR UPDATE ON
sandbox.simple_address FOR EACH ROW
WHEN ((pg_trigger_depth() < 1)) EXECUTE PROCEDURE
update_points_in_polygon(
'sandbox', 'polygon_address_units', 'address_count',
'simple_address');
INSERT INTO sandbox.polygon_address_units(wkb_geometry)
VALUES(ST_SetSRID(st_astext(st_geomfromtext('MULTIPOLYGON (((-1 1, 1 1, 1 -1, -1 -1, -1 1)))')),4326));
INSERT INTO sandbox.simple_address(wkb_geometry)
VALUES(ST_SetSRID(st_astext(st_geomfromtext('POINT(0 0)')),4326));
How do use these two queries to properly update the number_of_points column in my polygon layers when I add a point that intersects it ? There might be multiple polygons to update.
EDIT: The solution was to call tmprow to format the second string
FOR tmprow IN
EXECUTE format('SELECT c.objectid, count(a.*) point_count
FROM %I.%I a
JOIN %I.%I c
ON st_intersects(a.wkb_geometry, c.wkb_geometry)
WHERE st_intersects(c.wkb_geometry, st_geomFromText(''%s'', 4326))
GROUP BY c.objectid',
tables_schema, point_table_name, tables_schema, polygon_table_name,
st_astext(st_geomfromewkb(NEW.wkb_geometry)))
LOOP
EXECUTE format('UPDATE %I.%I c1
SET %I = %s
WHERE c1.objectid = %s ',
tables_schema, polygon_table_name, polygon_point_count_column_name, tmprow.point_count, tmprow.objectid);
END LOOP;
RETURN NEW;
END;
$function$

Related

Set value using select

I am new to using postgresql, I am trying to make a trigger that just inserts in the Employee table and also inserts in the Vacations table, but I don't know how to assign the values, I do it like that in sql but here I really don't know how
CREATE FUNCTION SP_InsertaVacacionesEmpleado() RETURNS TRIGGER
AS
$$
DECLARE _NumeroIdentificacion INTEGER;
DECLARE _FechaEntrada DATE;
BEGIN
SET _NumeroIdentificacion = SELECT NEW.NumeroIdentificacion FROM "Empleado"
SET _FechaEntrada = SELECT NEW.FechaEntrada FROM "Empleado"
INSERT INTO Vacaciones VALUES(_NumeroIdentificacion, _FechaEntrada, '', 0);
RETURN NEW;
END
$$
LANGUAGE plpgsql
As documented in the manual assignment is done using the := operator, e.g.:
some_variable := 42;
However to assign one or more variables from the result of a query, use select into, e.g.:
DECLARE
var_1 INTEGER;
var_2 DATE;
BEGIN
select col1, col2
into var_1, var_2
from some_table
...
However neither of that is necessary in a trigger as you can simply use the reference to the NEW record directly in the INSERT statement:
CREATE FUNCTION sp_insertavacacionesempleado()
RETURNS TRIGGER
AS
$$
BEGIN
INSERT INTO Vacaciones (...)
VALUES (NEW.NumeroIdentificacion, NEW.FechaEntrada , '', 0);
RETURN NEW;
END
$$
LANGUAGE plpgsql;
Note that you need to define a row level trigger for this to work:
create trigger ..
before insert on ...
for each row --<< important!
execute procedure sp_insertavacacionesempleado() ;

postgresql for loop script in text form can not be executed

I am trying to write function in postgresql, that creates temp_table with columns table_name text, table_rec jsonb and fill it through for loop with table names from my table containing names of tables and records in json. I have the for loop in string and I want to execute it. But it doesnt work.
I have variable rec record, sql_query text and tab_name text and I want to do this:
CREATE OR REPLACE FUNCTION public.test51(
)
RETURNS TABLE(tabel_name text, record_json jsonb)
LANGUAGE 'plpgsql'
COST 100
VOLATILE
ROWS 1000
AS $BODY$
declare
rec record;
tabel_name text;
tabel_names text[];
counter integer := 1;
sql_query text;
limit_for_sending integer;
rec_count integer;
begin
select into tabel_names array(select "TABLE_NAME" from public."TABLES");
create temp table temp_tab(tab_nam text, recik jsonb);
while array_length(tabel_names, 1) >= counter loop
tabel_name := '"' || tabel_names[counter] || '"';
select into limit_for_sending "TABLE_LIMIT_FOR_SENDING_DATA" from public."TABLES" where "TABLE_NAME" = tabel_name;
sql_query := 'select count(*) from public.' || tabel_name;
execute sql_query into rec_count;
if (rec_count >= limit_for_sending and limit_for_sending is not null) then
sql_query := 'for rec in select * from public.' || tabel_name || '
loop
insert into temp_tab
select ' || tabel_name || ', to_jsonb(rec);
end loop';
execute sql_query;
end if;
counter := counter + 1;
end loop;
return query
select * from temp_tabik;
drop table temp_tabik;
end;
$BODY$;
Thank you for response.
It seems you have some table that contains the information for which tables you want to return all rows as JSONB. And that meta-table also contains a column that sets a threshold under which the rows should not be returned.
You don't need the temp table or an array to store the table names. You can iterate through the query on the TABLES table and run the dynamic SQL directly in that loop.
return query in PL/pgSQL doesn't terminate the function, it just appends the result of the query to the result of the function.
Dynamic SQL is best created using the format() function because it is easier to read and using the %I placeholder will properly deal with quoted identifiers (which is really important as you are using those dreaded upper case table names)
As far as I can tell, your function can be simplified to:
CREATE OR REPLACE FUNCTION public.test51()
RETURNS TABLE(tabel_name text, record_json jsonb)
LANGUAGE plpgsql
AS
$BODY$
declare
rec record;
sql_query text;
rec_count bigint;
begin
for rec in
select "TABLE_NAME" as table_name, "TABLE_LIMIT_FOR_SENDING_DATA" as rec_limit
from public."TABLES"
loop
if rec.rec_limit is not null then
execute format('select count(*) from %I', rec.table_name)
into rec_count;
end if;
if (rec.rec_limit is not null and rec_count >= rec.rec_limit) then
sql_query := format('select %L, to_jsonb(t) from %I as t', rec.table_name, rec.table_name);
return query execute sql_query;
end if;
end loop;
end;
$BODY$;
Some notes
the language name is an identifier and should not be enclosed in single quotes. This syntax is deprecated and might be removed in a future version so don't get used to it.
you should really avoid those dreaded quoted identifiers. They are much more trouble than they are worth it. See the Postgres wiki for details.

Declare a Table as a variable in a stored procedure?

I am currently working a stored procedure capable of detecting continuity on a specific set of entries..
The specific set of entries is extracted from a sql query
The function takes in two input parameter, first being the table that should be investigated, and the other being the list of ids which should be evaluated.
For every Id I need to investigate every row provided by the select statement.
DROP FUNCTION IF EXISTS GapAndOverlapDetection(table_name text, entity_ids bigint[]);
create or replace function GapAndOverlapDetection ( table_name text, enteity_ids bigint[] )
returns table ( entity_id bigint, valid tsrange, causes_overlap boolean, causes_gap boolean)
as $$
declare
x bigint;
var_r record;
begin
FOREACH x in array $2
loop
EXECUTE format('select entity_id, valid from' ||table_name|| '
where entity_id = '||x||'
and registration #> now()::timestamp
order by valid ASC') INTO result;
for var_r in result
loop
end loop;
end loop ;
end
$$ language plpgsql;
select * from GapAndOverlapDetection('temp_country_registration', '{1,2,3,4}')
I currently get an error in the for statement saying
ERROR: syntax error at or near "$1"
LINE 12: for var_r in select entity_id, valid from $1
You can iterate over the result of the dynamic query directly:
create or replace function gapandoverlapdetection ( table_name text, entity_ids bigint[])
returns table (entity_id bigint, valid tsrange, causes_overlap boolean, causes_gap boolean)
as $$
declare
var_r record;
begin
for var_r in EXECUTE format('select entity_id, valid
from %I
where entity_id = any($1)
and registration > now()::timestamp
order by valid ASC', table_name)
using entity_ids
loop
... do something with var_r
-- return a row for the result
-- this does not end the function
-- it just appends this row to the result
return query
select entity_id, true, false;
end loop;
end
$$ language plpgsql;
The %I injects an identifier into a string and the $1 inside the dynamic SQL is then populated through passing the argument with the using keyword
Firstly, decide whether you want to pass the table's name or oid. If you want to identify the table by name, then the parameter should be of text type and not regclass.
Secondly, if you want the table name to change between executions then you need to execute the SQL statement dynamically with the EXECUTE statement.

Auto Table Partitioning -PostgreSQL- ERROR: too few arguments for format()

I'm trying to auto partition my oltpsales table using a function below and a trigger below but then I try to perform in insert on the table I get error code below. I have referenced a few threads below and suggestions are welcomed.
INSERT with dynamic table name in trigger function
Postgres format string using array
ERROR: too few arguments for format() CONTEXT: PL/pgSQL function
testoltpsales_insert_function() line 17 at EXECUTE
CREATE TRIGGER testoltpsales_insert_trg
BEFORE INSERT ON myschema."testoltpsales"
FOR EACH ROW EXECUTE PROCEDURE testoltpsales_insert_function();
CREATE OR REPLACE FUNCTION testoltpsales_insert_function()
RETURNS TRIGGER AS $$
DECLARE
partition_date TEXT;
partition_name TEXT;
start_of_month TEXT;
end_of_next_month TEXT;
BEGIN
partition_date := to_char(NEW."CreateDateTime",'YYYY_MM');
partition_name := 'testoltpsaless_' || partition_date;
start_of_month := to_char((NEW."CreateDateTime"),'YYYY-MM') || '-01';
end_of_next_month := to_char((NEW."CreateDateTime" + interval '1 month'),'YYYY-MM') || '-01';
IF NOT EXISTS
(SELECT 1
FROM information_schema.tables
WHERE table_name = partition_name)
THEN
EXECUTE format(E'CREATE TABLE %I (CHECK ( date_trunc(\'day\', %I.CreateDateTime) >= ''%s'' AND date_trunc(\'day\', %I.CreateDateTime) < ''%s'')) INHERITS (myschema."Testoltpsaless")',
VARIADIC ARRAY [partition_name, start_of_month,end_of_next_month]);
RAISE NOTICE 'A partition has been created %', partition_name;
-- EXECUTE format('GRANT SELECT ON TABLE %I TO readonly', partition_name); -- use this if you use role based permission
END IF;
EXECUTE format('INSERT INTO %I ("OwnerId","DaddyTable","SaleId","RunId","CreateDateTime","SalesetId","Result","Score","NumberOfMatches" ) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9)', partition_name)
USING NEW."OwnerId",NEW."DaddyTable",NEW."SaleId",NEW."RunId",NEW."CreateDateTime",NEW."SalesetId",NEW."Result",NEW."Score",NEW."NumberOfMatches";
RETURN NULL;
END
$$
LANGUAGE plpgsql;
EXECUTE format(E'CREATE TABLE %I (CHECK ( date_trunc(\'day\', %I.CreateDateTime) >= ''%s'' AND date_trunc(\'day\',
%I.CreateDateTime) < ''%s'')) INHERITS (myschema."Testoltpsaless")',
VARIADIC ARRAY [partition_name, start_of_month,end_of_next_month]); ```
There are five format specifiers in your format string but you're passing it only three arguments. Unless you're using positional formatting e.g. %1$I, you must supply the same number of args, as they are used sequentially.
https://www.postgresql.org/docs/current/functions-string.html#FUNCTIONS-STRING-FORMAT

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.