Pl/Pgsql, Passing array argument to INSERT - postgresql

Say I have a function with a text array parameter TEXT[]. If I do a EXECUTE FORMAT INSERT, how do I pass a quoted text string of that array to insert?

You should to use USING clause. The dynamic SQL can use a parameters on usual places (non SQL identifiers):
CREATE TABLE foo(a varchar[]);
CREATE OR REPLACE FUNCTION public.fx(tblname text, VARIADIC p character varying[])
RETURNS void LANGUAGE plpgsql AS $function$
BEGIN
EXECUTE format('insert into %I(a) VALUES($1)', tblname) USING p;
END;
$function$
SELECT fx('foo', 'Hi','Hello');
SELECT fx('foo', 'Hi','Hel''lo');
SELECT fx('foo', 'Hi','Hel"lo');
postgres=# SELECT * FROM foo;
┌────────────────┐
│ a │
╞════════════════╡
│ {Hi,Hel'lo} │
│ {Hi,"Hel\"lo"} │
│ {Hi,Hello} │
└────────────────┘
(3 rows)

Thank you, I now know when to use using, and format. Here's my revised code:
CREATE OR REPLACE FUNCTION add_property(catid INT, colname TEXT,
ty catalog_column_type, colval TEXT[])
RETURNS jsonb AS $$
DECLARE
tn TEXT;
BEGIN
--check table exists
SELECT tablename INTO tn FROM catalog WHERE catalogid=catid;
IF NOT EXISTS(SELECT 1 FROM information_schema.tables WHERE table_name=tn)
THEN
return jsonb_build_object('error','notable');
END IF;
--check if property exists for table
IF EXISTS(SELECT 1 FROM catalog_columns WHERE catalogid=catid AND
columnname=colname) THEN
return jsonb_build_object('error','exists');
END IF;
IF ty='INT'::catalog_column_type THEN
EXECUTE FORMAT ('ALTER TABLE %I ADD COLUMN %I INT',tn,colname);
ELSIF ty='TEXT'::catalog_column_type THEN
EXECUTE FORMAT ('ALTER TABLE %I ADD COLUMN %I TEXT',tn,colname);
ELSIF ty='ENUM'::catalog_column_type THEN
EXECUTE FORMAT ('ALTER TABLE %I ADD COLUMN %I INT',tn,colname);
ELSIF ty='BOOLEAN'::catalog_column_type THEN
EXECUTE FORMAT ('ALTER TABLE %I ADD COLUMN %I BOOLEAN',tn,colname);
END IF;
EXECUTE 'INSERT INTO catalog_columns (catalogid,columnname,'
|| 'columntype,columnnvalues) VALUES ($1,$2,$3,$4)' USING catid,colname,
ty,colval;
return jsonb_build_object('error','OK');
END;
$$ LANGUAGE plpgsql;

Related

plpgsql: Cast 'new' record for insert statement

I try to build a history inside a table. Part of the history function are the following table attributes:
id: uuid
historycounter: int8
Each time a dataset is added to the table its historycounter is set to zero.
Each time a dataset is update the historycounter should be increased by one.
A before insert or update or delete on <table> for each row should do all the work.
For the update part of the trigger function this looks as follows:
execute format('update %I.%I set historycounter = historycounter + 1 where id = $1', schema_name, tab_name) using old.id;
-- execute format('insert into %I.%I select $1', schema_name, tab_name) using new::text:xx;
execute format('insert into %I.%I values ($1)', schema_name, tab_name) using new.*::text;
return null;
Updating the historycounter works fine. But what ever I try, I did not find a way to do the conversion from UPDATE into INSERT. I like to build it on the most generic way possible. So knowing and adding each attribute of the dataset (table) is not a way.
As far as I know new and old are of the type record. Casting a record to row should do the trick, but HOW?
Any hints are welcome!
You need to unpack composite value inside INSERT statement. This is SQL feature (not plpgsql feature). So using star * in USING clause should not to work. NEW and OLD are not record variables. They are composite variables with known type.
create table foo(a int, b varchar, c int);
create table foo2( a int, b varchar, c int);
create or replace function foo_insert_trg()
returns trigger as $$
begin
execute 'insert into foo2 values($1.*)' using new;
return null;
end;
$$ language plpgsql;
insert into foo values(10, 'ahoj', 20);
INSERT 0 0
select * from foo2;
┌────┬──────┬────┐
│ a │ b │ c │
╞════╪══════╪════╡
│ 10 │ ahoj │ 20 │
└────┴──────┴────┘
(1 row)

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.

Pass a schema name and table name dynamically in FROM in a select query in Postgres

Pass a schema name and table name dynamically in FROM in a select query in postgres.
I need to call a table dynamically in a from (in the select clause)
CREATE OR REPLACE FUNCTION xx.fn_build_test_(
IN p_var_archive_schema character varying,
IN p_var_archive_table character varying)
RETURNS record AS
$BODY$
declare
l_var_archive_schema VARCHAR;
l_var_archive_table VARCHAR;
l_var_test VARCHAR[];
BEGIN
l_var_archive_schema := p_var_archive_schema;
l_var_archive_table := p_var_archive_table;
SELECT array
( SELECT TO_CHAR(column_name,'YYYYMMDD')
FROM "test_table"
WHERE col1 = 1)
INTO l_var_test;
END;
$BODY$
LANGUAGE plpgsql
VOLATILE SECURITY INVOKER;
I need values for:
l_var_archive_schema VARCHAR;
l_var_archive_table VARCHAR;
in place of the test table
You do not need those local variables for schema and table.
Use format option to construct the queries and EXECUTE to run it dynamically
CREATE OR REPLACE FUNCTION xx.fn_build_test_(
IN p_var_archive_schema character varying,
IN p_var_archive_table character varying )
RETURNS record AS
$BODY$
DECLARE
l_var_test VARCHAR[];
BEGIN
SELECT array
( SELECT TO_CHAR(column_name,'YYYYMMDD')
FROM "test_table"
WHERE col1 = 1
) INTO l_var_test;
EXECUTE format (
'select col_name FROM %I.%I',
p_var_archive_schema,p_var_archive_table)
--INTO rec_variable;
END;
$BODY$
LANGUAGE plpgsql
VOLATILE SECURITY INVOKER;
If you want to return the result of a dynamic query you may use
RETURNS TABLE option and then do RETURN QUERY EXECUTE to return results from the query.
You can use dynamic SQL. For example change the SQL in the function as following. The function needs further correction as by definition it is supposed to return a record but currently not returning any value.
EXECUTE 'SELECT array
( SELECT TO_CHAR(column_name,''YYYYMMDD'')
FROM '||l_var_archive_schema||'.'||l_var_archive_table||' WHERE col1 = 1)'
INTO l_var_test;

How to return multiple INSERTED ID's in Postgresql?

I have a function which takes two arguments, the first one is an integer and the second one is an array of varchars.
I want to insert just the hashes that aren't previously inserted for the campaign and then return the inserted ids — in this case, the url_hash field of the campaigns_urls table — but I keep getting the following error:
ERROR: column "hash" does not exist LINE 10: RETURNING "hash"
^
HINT: There is a column named "hash" in table "*SELECT*", but it cannot be referenced from this part of the query.
I am calling a function like this:
-- SELECT * FROM assign_urls_to_campaign(1,'{Xelgb20Lw}')
CREATE OR REPLACE FUNCTION public.assign_urls_to_campaign(
param_campaign_id integer,
param_hashes character varying(20)[]
)
RETURNS character varying(20)
LANGUAGE 'plpgsql'
VOLATILE
AS $BODY$
BEGIN
INSERT INTO campaigns_urls ("campaign_id", "url_hash")
SELECT
param_campaign_id as "id", "P"."hash"
FROM "urls" AS "U"
RIGHT OUTER JOIN (
SELECT hash FROM UNNEST(param_hashes) AS "hash"
) AS "P"
ON "U"."hash" = "P"."hash"
WHERE "U"."hash" ISNULL
RETURNING "hash";
END;
$BODY$;
There are more issues:
If functions returns more than one row, then should to use SETOF keywords after RETURNS.
PlpgSQL functions requires RETURN statement - in this case RETURN QUERY.
create table test(a int);
create or replace function foo(int)
returns setof int as $$
begin
return query
insert into test
select v from generate_series(1,$1) g(v)
returning a;
end;
$$ language plpgsql;
postgres=# select * from foo(3);
┌─────┐
│ foo │
╞═════╡
│ 1 │
│ 2 │
│ 3 │
└─────┘
(3 rows)

Postgresql plpgsql multiple row loop

I'm busy trying to rewrite an Informix stored procedure for a PostgreSQL
database and I am stuck on something that is probably quite obvious to
everyone who know PostgreSQL.
I have my sql script as follows
-- ensure type and function get created
drop type if exists tp_users cascade;
drop function if exists sp_cmplist();
-- create type
create type tp_users as (
us_id char(30),
us_status char(1)
);
create function sp_cmplist()
returns tp_users as $$
declare
lr_users tp_users;
begin
for lr_users in
select users.us_id, users.us_status
from users
loop
return lr_users;
end loop;
end
$$ language 'plpgsql';
select sp_cmplist();
this is just a dummy script to select from an imaginary users table but how would I use this script with a cursor or loop to make sure all results are returned?
This code works:
CREATE TABLE foo(a int);
INSERT INTO foo VALUES(10),(20);
CREATE OR REPLACE FUNCTION retfoo()
RETURNS SETOF foo AS $$
BEGIN
RETURN QUERY SELECT * FROM foo;
RETURN;
END;
$$ LANGUAGE plpgsql;
postgres=# SELECT * FROM retfoo();
┌────┐
│ a │
├────┤
│ 10 │
│ 20 │
└────┘
(2 rows)
Time: 1.143 ms
I may have answered my own question with the following
drop type if exists tp_users cascade;
drop function if exists sp_cmplist();
create type tp_users as (
us_id text,
us_status text,
lv_nothing text,
lv_cnt int
);
create function sp_cmplist()
returns setof tp_users as $$
declare
lr_users tp_users;
lv_cnt int;
begin
lv_cnt := 0;
for lr_users in
select users.us_id, users.us_status
from users
loop
-- increment this counter for testing purposes
lv_cnt := lv_cnt + 1;
lr_users.lv_nothing := 'yupy';
lr_users.lv_cnt := lv_cnt;
return next lr_users;
end loop;
return;
end
$$ language 'plpgsql';
select * from sp_cmplist();
this seems to work perfectly