plpgsql: Cast 'new' record for insert statement - postgresql

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)

Related

How to log 'new table' from update trigger in postgresql?

I have a complex trigger which executes on update of a table.
I get what I want when I execute it's body with 'new table' replaced with existing table, but it messes things up when it called by update.
I want to debug this and I want to start by viewing what my trigger gets as 'new table'. How can I look on it?
CREATE TRIGGER foo_trigger AFTER
UPDATE
ON
public.table1 REFERENCING NEW TABLE AS new_table FOR EACH STATEMENT EXECUTE FUNCTION foo()
CREATE OR REPLACE FUNCTION public.foo()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
BEGIN
*do things with new_table*
RETURN NULL;
END;
$function$
;
You can dump it to a table and check its contents after your test update.
create table public.table1 (col1 integer);
insert into public.table1 values (0),(1),(2),(3);
create table public.foo_trigger_test as table public.table1 with no data;
CREATE OR REPLACE FUNCTION public.foo()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
BEGIN
insert into public.foo_trigger_test select a.* from new_table a;
RETURN NULL;
END;
$function$;
CREATE TRIGGER foo_trigger
AFTER UPDATE
ON public.table1
REFERENCING NEW TABLE AS new_table
FOR EACH STATEMENT
EXECUTE FUNCTION foo();
Now an update triggering the function will dump a copy of what it got in NEW (aliased by new_table).
update public.table1 set col1=99 where col1 between 0 and 2;
table public.table1;
-- col1
--------
-- 3
-- 99
-- 99
-- 99
--(4 rows)
table public.foo_trigger_test;
-- col1
--------
-- 99
-- 99
-- 99
--(3 rows)
Example

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)

Pl/Pgsql, Passing array argument to INSERT

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;

Postgres create universal function on all table

I have a few db tables.
I want write universtal postgres function on copy rows to history tables
I have tables:
table1
table1_h
table2
table2_h
I wrote function (with help stackoverflow)
CREATE OR REPLACE FUNCTION copy_history_f() RETURNS TRIGGER AS
$BODY$
DECLARE
tablename_h text:= TG_TABLE_NAME || '_h';
BEGIN
EXECUTE 'INSERT INTO ' || quote_ident(TG_TABLE_SCHEMA) || '.' || quote_ident(tablename_h) || ' VALUES (' || OLD.* ||')';
RETURN NULL;
END;
$BODY$
LANGUAGE plpgsql VOLATILE;
And functions was create, but after update is error.
ERROR: syntax error at or near ","
ROW 1: ...RT INTO table1_h VALUES ((12,,,0,,"Anto...
I know where is error in this insert but I don't know how I repair that.
Structure tables table1 and table1_h are identical but table1_h has one more column (id_h)
Can you help me, how I have create psql function?
Thnak you.
drop table if exists t;
drop table if exists t_h;
drop function if exists ftg();
create table t(i serial, x numeric);
insert into t(x) values(1.1),(2.2);
create table t_h(i int, x numeric);
create function ftg() returns trigger language plpgsql as $ftg$
declare
tablename_h text:= TG_TABLE_NAME || '_h';
begin
execute format($q$ insert into %I.%I select $1.*; $q$, TG_TABLE_SCHEMA, tablename_h) using old;
return null;
end $ftg$;
create trigger tg_t after delete on t for each row execute procedure ftg();
delete from t where i = 1;
select * from t_h;
dbfiddle
Update It solves your problem, but I think that you want to have a bit more info in your history tables. It will be more complex a bit:
drop table if exists t;
drop table if exists t_h;
drop function if exists ftg();
create table t(i serial, x numeric);
insert into t(x) values(1.1),(2.2);
create table t_h(
hi serial, -- just ID
hd timestamp, -- timestamp
hu text, -- user who made changes
ha text, -- action
i int, x numeric
);
create function ftg() returns trigger language plpgsql as $ftg$
declare
tablename_h text:= TG_TABLE_NAME || '_h';
begin
execute format(
$q$
insert into %I.%I
select
nextval(%L || '_hi_seq'),
clock_timestamp(),
current_user,
%L,
$1.*
$q$, TG_TABLE_SCHEMA, tablename_h, tablename_h, TG_OP) using old;
return null;
end $ftg$;
create trigger tg_t after delete or update on t for each row execute procedure ftg();
update t set x = x * 2;
update t set x = x * 2 where i = 2;
delete from t where i = 1;
select * from t_h;
dbfiddle
I assume you are inserting the 'old' values from table1 into table1_h.
The additional column is your problem. When you using an insert without naming columns you must use a matching number and type for the insert.
You must use column referencing.
eg.
Insert into table1_h(column1, column2, column3)
values (a,b,c)
Consider a default value for the additional column in table table1_h.

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