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

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

Related

Create partition table using execute

I would like to create N partition tables for the last N days. I have created a table like the following
create table metrics.my_table (
id bigserial NOT NULL primary key,
...
logdate date NOT NULL
) PARTITION BY LIST (logdate);
Then I have the following function to create those tables:
CREATE OR REPLACE function metrics.create_my_partitions(init_date numeric default 30, current_date_parameter timestamp default current_date)
returns void as $$
DECLARE
partition_date TEXT;
partition_name TEXT;
begin
for cnt in 0..init_date loop
partition_date := to_char((current_date_parameter - (cnt * interval '1 day')),'YYYY-MM-DD');
raise notice 'cnt: %', cnt;
raise notice 'partition_date: %', partition_date;
partition_name := 'my_table_' || partition_date;
raise notice 'partition_name: %', partition_name;
EXECUTE format('CREATE table if not exists metrics.%I PARTITION OF metrics.my_table for VALUES IN ($1)', partition_name) using partition_date;
end loop;
END
$$
LANGUAGE plpgsql;
select metrics.create_my_partitions(30, current_date);
But it throws the following error in the EXECUTE format line:
SQL Error [42P02]: ERROR: there is no parameter $1
Any idea on how to create those tables?
The EXECUTE ... USING ... option only works for data values in DML commands (SELECT,INSERT, etc.). Since CREATE TABLE is a DDL command, use a parameter in format():
execute format(
'create table if not exists metrics.%I partition of metrics.my_table for values in (%L)',
partition_name, partition_date);

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.

How to convert a PL/PgSQL procedure into a dynamic one?

I am trying to write a plpgsql procedure to perform spatial tiling of a postGIS table. I can perform the operation successfully using the following procedure in which the table names are hardcoded. The procedure loops through the tiles in tile_table and for each tile clips the area_table and inserts it into split_table.
CREATE OR REPLACE PROCEDURE splitbytile()
AS $$
DECLARE
tile RECORD;
BEGIN
FOR tile IN
SELECT tid, geom FROM test_tiles ORDER BY tid
LOOP
INSERT INTO split_table (id, areaname, ttid, geom)
SELECT id, areaname, tile.tid,
CASE WHEN st_within(base.geom, tile.geom) THEN st_multi(base.geom)
ELSE st_multi(st_intersection(base.geom, tile.geom)) END as geom
FROM area_table as base
WHERE st_intersects(base.geom, tile.geom);
COMMIT;
END LOOP;
END;
$$ LANGUAGE 'plpgsql';
Having tested this successfully, now I need to convert it to a dynamic procedure where I can provide the table names as parameters. I tried the following partial conversion, using format() for inside of loop:
CREATE OR REPLACE PROCEDURE splitbytile(in_table text, grid_table text, split_table text)
AS $$
DECLARE
tile RECORD;
BEGIN
FOR tile IN
EXECUTE format('SELECT tid, geom FROM %I ORDER BY tid', grid_table)
LOOP
EXECUTE
FORMAT(
'INSERT INTO %1$I (id, areaname, ttid, geom)
SELECT id, areaname, tile.tid,
CASE WHEN st_within(base.geom, tile.geom) THEN st_multi(base.geom)
ELSE st_multi(st_intersection(base.geom, tile.geom)) END as geom
FROM %2$I as base
WHERE st_intersects(base.geom, tile.geom)', split_table, in_table
);
COMMIT;
END LOOP;
END;
$$ LANGUAGE 'plpgsql';
But it throws an error
missing FROM-clause entry for table "tile"
So, how can I convert the procedure to a dynamic one? More specifically, how can I use the record data type (tile) returned by the for loop inside the loop? Note that it works when format is not used.
You can use EXECUTE ... USING to supply parameters to a dynamic query:
EXECUTE
format(
'SELECT r FROM %I WHERE c = $1.val',
table_name
)
INTO result_var
USING record_var;
The first argument to USING will be used for $1, the second for $2 and so on.
See the documentation for details.
Personally I use somehow different way to create dynamic functions. By concatination and execute function. You can also do like this.
CREATE OR REPLACE FUNCTION splitbytile()
RETURNS void AS $$
declare
result1 text;
table_name text := 'test_tiles';
msi text := '+7 9912 231';
msi text := 'Hello world';
code text := 'code_name';
_operator_id integer := 2;
begin
query1 := 'SELECT msisdn from ' || table_name || ' where msisdn = ''' || msi::text ||''';';
query2 := 'INSERT INTO ' || table_name || '(msisdn,usage,body,pr_code,status,sent_date,code_type,operator_id)
VALUES( ''' || msi::text || ''',' || true || ',''' || _body::text || ''',''' || code::text || ''',' || false || ',''' || time_now || ''',' || kod_type || ',' || _operator_id ||');';
execute query1 into result1;
execute query2;
END;
$function$
You just make your query as text then anywhere you want you can execute it. Maybe by checking result1 value inside If statement or smth like that.

Execute decode function stored in bytea column

I have a bytea column in a table that contains a function decode(). What I have done to get the actual data is as follows:
select filename, convert_from(data,'UTF-8') from attachments limit 20; //this returns me decode function
select decode(E'...','hex'); // I am executing the above returned function
The above is fine as long as I have to select one row. But now my requirement is to get more than one result. How can I get the result in single query? I have tried using pl/pgsql
CREATE OR REPLACE FUNCTION get_data(integer, _type anyelement, OUT _result anyelement)
AS
$x$
BEGIN
EXECUTE
'SELECT ' || (select convert_from(data,'UTF-8') as data from attachments limit $1)
INTO _result;
END;
$x$
LANGUAGE plpgsql;
But this works only for single row and single column. What I want is a single query to fetch 2 columns without using pl/pgsql if possible. I am using this query from my Java based web app.
Thanks!
You need procedural code for this, since there is no provision for dynamic statements in SQL.
The following function converts all attachments:
CREATE FUNCTION getemall(
IN v_type anyelement,
OUT v_result anyelement
) RETURNS SETOF anyelement
LANGUAGE plpgsql AS
$$DECLARE
v_stmt text;
BEGIN
FOR v_stmt IN
SELECT convert_from(data,'UTF-8')
FROM attachments
LOOP
EXECUTE v_stmt INTO v_result;
RETURN NEXT;
END LOOP;
END;$$;
This is how I have written the function with few changes
CREATE OR REPLACE FUNCTION getmeall(tName text, fNameCol text, dataCol text,fSize
numeric)
RETURNS TABLE(bdata bytea, fname text) LANGUAGE plpgsql AS
$$DECLARE
v_stmt text;
v_name text;
BEGIN
FOR v_stmt,v_name IN
EXECUTE format('SELECT encode(%s, ''escape''), %s FROM %s
WHERE $1 IS NOT NULL AND $2 IS NOT NULL LIMIT $3'
, dataCol, fNameCol, tName)
USING dataCol, fNameCol, fSize
LOOP
fname:=v_name;
IF strpos(v_stmt,'decode') = 1 THEN
EXECUTE 'SELECT ' || v_stmt INTO bdata;
ELSE
bdata:=v_stmt;
END IF;
RETURN NEXT;
END LOOP;
END;$$;
And finally calling it this way.
select * from getmeall('attachments', '"filename"', '"data"',2)

Inserting into dynamic table partition in postgresql with return

I created a table partition that will create a table if it is not yet existing the table names are on a monthly basis. I need this function to return the inserted ID but I'm getting this error of column "partition" does not exist it seems that my schema(partition) is considered column in this code
CREATE OR REPLACE FUNCTION partition.itinerary_partition_function()
RETURNS TRIGGER AS
$BODY$
DECLARE
reflowId bigint;
_tablename text;
_startyear text;
_startmonth text;
_fulltablename text;
BEGIN
--Takes the current inbound "time" value and determines when midnight is for the given date
_startyear := to_char(now(), 'YYYY');
_startmonth := to_char(now(), 'MM');
_tablename := 'itinerary_'||_startyear || '_' || _startmonth;
_fulltablename := 'partition.' || _tablename;
-- Check if the partition needed for the current record exists
PERFORM 1
FROM pg_catalog.pg_class c
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind = 'r'
AND c.relname = _tablename
AND n.nspname = 'partition';
-- If the partition needed does not yet exist, then we create it:
-- Note that || is string concatenation (joining two strings to make one)
IF NOT FOUND THEN
EXECUTE 'CREATE TABLE partition.' || quote_ident(_tablename) || '()INHERITS (partition.itinerary)';
-- Table permissions are not inherited from the parent.
-- If permissions change on the master be sure to change them on the child also.
EXECUTE 'ALTER TABLE partition.' || quote_ident(_tablename) || ' OWNER TO postgres';
-- Indexes are defined per child, so we assign a default index that uses the partition columns
EXECUTE 'CREATE INDEX ' || quote_ident(_tablename||'_indx1') || ' ON partition.' || quote_ident(_tablename) || ' (id)';
END IF;
BEGIN
EXECUTE format('INSERT INTO %I SELECT $1.*', "partition." || _tablename)
USING NEW;
RETURN NEW;
END;
END;
$BODY$
LANGUAGE plpgsql;
After this code I am calling it in another insert function
CREATE OR REPLACE FUNCTION partition.insert_data(username text,jsonData jsonb) RETURNS bigint AS
$$
DECLARE reflowId bigint;
BEGIN
INSERT INTO reflow_partition.itinerary(username, data)
VALUES (username, jsonData) RETURNING id;
END;
$$
LANGUAGE plpgsql;
Try changing this:
EXECUTE format('INSERT INTO %I SELECT $1.*', "partition." || _tablename)
to this:
EXECUTE format('INSERT INTO %1$I.%2$I SELECT $1.*', 'partition', _tablename)