pl/pgsql: How show values of NEW & OLD trigger record if i don't know the name of original table colum? - sql

Hi all,
I wrote a function in pl/pgsql and I have this problem:
I want use the values of NEW and OLD trigger record but I don't know the column name and number if the table.
for example:
CREATE OR REPLACE FUNCTION tt() RETURNS trigger AS $$
DECLARE
text1 text;
text2 text;
orig_name text =tg_argv[0];
orig_schema text =tg_argv[1];
log_name text =tg_argv[2];
log_schema text =tg_argv[3];
col pg_attribute.attname%TYPE;
[...]
BEGIN
orig_comp := quote_ident(orig_schema)||'.'||quote_ident(orig_name);
log_comp := quote_ident(log_schema)||'.'||quote_ident(log_name);
IF(trigger_mode='INSERT')
THEN
-- I want know the names of column
FOR colonna in
SELECT attname
FROM pg_attribute
WHERE attrelid = orig_comp::regclass
AND attnum > 0
AND NOT attisdropped
loop --for each column I want save the value like a string
text1=NEW||'.'||colonna; -- this don't work: error: record NEW don't have colonna like values
text2:=text2||' , '||text1;
end loop;
[...]
END IF;
[...]
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

you know a original name - it is a variable TG_TABLE_NAME. And dynamic access to record fields is possible with EXECUTE USING statement.
CREATE OR REPLACE FUNCTION dynamic_trigger()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
ri RECORD;
t TEXT;
BEGIN
RAISE NOTICE E'\n Operation: %\n Schema: %\n Table: %',
TG_OP,
TG_TABLE_SCHEMA,
TG_TABLE_NAME;
FOR ri IN
SELECT ordinal_position, column_name, data_type
FROM information_schema.columns
WHERE
table_schema = quote_ident(TG_TABLE_SCHEMA)
AND table_name = quote_ident(TG_TABLE_NAME)
ORDER BY ordinal_position
LOOP
EXECUTE 'SELECT ($1).' || ri.column_name || '::text' INTO t USING NEW;
RAISE NOTICE E'Column\n number: %\n name: %\n type: %\n value: %.',
ri.ordinal_position,
ri.column_name,
ri.data_type,
t;
END LOOP;
RETURN NEW;
END; $$;
This code is wrote by Tom Lane and it is from postgresql tricks pages Iteration over RECORD variable inside trigger.

Related

PL/pgSQL select statement inside trigger returns null using where NEW

i'm creating a trigger that triggers on INSERT on a table,
and i wish to log the structure of tables inserted so i wrote this Function
CREATE OR REPLACE FUNCTION update_table_log_received()
RETURNS TRIGGER AS $$
DECLARE
added_column TEXT;
target_table_name TEXT;
old_column text;
BEGIN
-- Check if a new column has been added
IF (TG_OP = 'INSERT') THEN
added_column := NEW."COLUMN_NAME";
target_table_name := NEW."TABLE_NAME";
END IF;
SELECT column_name into old_column
FROM information_schema."columns"
WHERE table_schema = 'items'
and table_name = LOWER(NEW."TABLE_NAME")
and column_name = LOWER(NEW."COLUMN_NAME");
if (coalesce(old_column,'')='' or old_column='' or old_column = added_column) THEN
-- If a new column has been added
IF (Lower(added_column) != 'sync') then
-- Add the new column to the target table
EXECUTE 'ALTER TABLE items.' || LOWER(target_table_name)|| ' ADD COLUMN ' || LOWER(added_column) || ' VARCHAR(50)';
END IF;
end if;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
executed by this TRIGGER :
CREATE TRIGGER update_table_log_received_trigger
AFTER INSERT
ON items."TABLE_LOG_RECEIVED"
FOR EACH ROW
EXECUTE FUNCTION update_table_log_received();
the returned exception is the following :
! ERROR: the column « x » of the relation « y » already exists
Where: instruction SQL « ALTER TABLE items. ADD COLUMN x VARCHAR(50) »
my problem now is that it isn't supposed to pass the If checks (i pasted the code after many alterations i have two if conditions that do the same thing just because),
i debugged and logged the statements to note that the select query inside my function returns null apparently.
i also tried to use "USING NEW" but i am no expert so i couldn't make it work
is it a problem with the declared variable not being populated from the "NEW" record or am i executing the select statement wrong ?
EDIT : tl;dr for my problem, I would like to update a table in Database2 whenever the same table (that had the same structre) is altered from Database1, be it added column or changed column, at this point iI'm stuck at the first problem to add the column.
I am logging my tables' structures as strings into a new table and syncing that with Database2 to then have the trigger alter the same altered table from Database1, hope this makes more sense now.
Database1 log_table that logs all my tables' structures:
Database2 log_table_received that is a copy of log_table that executes
the trigger whenever new values are inserted;
Try this syntax:
CREATE OR REPLACE FUNCTION update_table_log_received()
RETURNS TRIGGER AS $$
DECLARE
added_column TEXT;
target_table_name TEXT;
old_column text;
BEGIN
-- Check if a new column has been added
IF (TG_OP = 'INSERT') THEN
added_column := new."column_name";
target_table_name := new."table_name";
END IF;
if not exists(select 1 from information_schema."columns" where table_name = target_table_name and column_name = added_column)
then
EXECUTE 'ALTER TABLE items.' || LOWER(target_table_name)|| ' ADD COLUMN ' || LOWER(added_column) || ' VARCHAR(50)';
end if;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
I tried on my DB this is works. You can change some details yourself.
Fixed; Question should have been :
How to select tables & table columns inside function in postgresql.
References:
How to add column if not exists on PostgreSQL?
How to check if a table exists in a given schema
How to get a list column names and datatypes of a table in PostgreSQL?
Basically information_schema can only be accessed by owner meaning the user or (i) see the result when i query it but it returns FALSE when executed inside a script more details here :
https://stackoverflow.com/a/24089729/15170264
Full trigger after fix with CTE to query the pg_catalog also added ADD COLUMN IF NOT EXISTS in my Execute query just to be safe
CREATE OR REPLACE FUNCTION update_table_log_received()
RETURNS TRIGGER AS $$
DECLARE
added_column TEXT;
target_table_name TEXT;
old_column varchar;
old_table varchar;
BEGIN
-- Check if a new column has been added
IF (TG_OP = 'INSERT') THEN
added_column := NEW."COLUMN_NAME";
target_table_name := NEW."TABLE_NAME";
END IF;
/*
* --------------- --CTE to find Columns of table "Target_table_name" from pg_catalog
*/
WITH cte_tables AS (
SELECT
pg_attribute.attname AS column_name,
pg_catalog.format_type(pg_attribute.atttypid, pg_attribute.atttypmod) AS data_type
FROM
pg_catalog.pg_attribute
INNER JOIN
pg_catalog.pg_class ON pg_class.oid = pg_attribute.attrelid
INNER JOIN
pg_catalog.pg_namespace ON pg_namespace.oid = pg_class.relnamespace
WHERE
pg_attribute.attnum > 0
AND NOT pg_attribute.attisdropped
AND pg_namespace.nspname = 'items'
AND pg_class.relname = 'trace'
ORDER BY
attnum ASC
)
select column_name into old_column from cte_tables where
column_name=LOWER(added_column);
if (old_column is null ) then
-- Add the new column to the target table
old_column := added_column;
EXECUTE 'ALTER TABLE items.' || LOWER(target_table_name)|| ' ADD COLUMN IF NOT EXISTS ' || LOWER(added_column) || ' VARCHAR(50)';
else
old_column := added_column || 'already exists ! ';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
create TRIGGER update_table_log_received_trigger
AFTER INSERT
ON items."TABLE_LOG_RECEIVED"
FOR EACH ROW
EXECUTE FUNCTION update_table_log_received();
Variable old_column stores the else condition message but i do not return it, would have if it was a simple function.

Dynamic column name postgresql trigger

I'm trying to create a dynamic audit trigger that reads a column_name from the information schema and inserts into an audit table which column has been changed. So far I have the following code:
CREATE OR REPLACE FUNCTION dynamic_column_audit()
RETURNS trigger
LANGUAGE 'plpgsql'
COST 100
VOLATILE NOT LEAKPROOF AS $BODY$
<<main_function>>
declare
table_name_tr text;
table_schema_tr text;
column_name_trigger text;
old_column_name_trigger record;
new_column_name_trigger record;
begin
-- variables
table_name_tr := TG_TABLE_NAME;
table_schema_tr := TG_TABLE_SCHEMA;
<<loop_sql>>
FOR column_name_trigger IN (SELECT c.column_name FROM information_schema.columns c WHERE c.table_schema = table_schema_tr and c.table_name = table_name_tr ORDER BY ordinal_position asc)
LOOP
RAISE INFO E'\n Column name: %, table_name: %', column_name_trigger , table_schema_tr||'.'||table_name_tr;
IF (TG_OP = 'UPDATE') THEN
INSERT INTO my_loggingtable (operation, table_schema, table_name, column_name, old_value, new_value)
VALUES (TG_OP, table_name_tr, table_schema_tr, column_name_trigger, OLD.||column_name_trigger, NEW.||column_name_trigger);
end if;
END LOOP loop_sql;
return new;
end main_function;
$BODY$;
I'm trying to see whether it's possible to do something like NEW. + column_name or OLD> + column_name from the loop rather than hardcode the column name value for each table.
The idea behind this, which it could not be possible is to do an audit trigger for around 20 tables without adding the name all the time.
You can use a construct like this:
DECLARE
newval text;
BEGIN
EXECUTE format(
'SELECT ($1::%I).%I',
table_name,
column_name
)
INTO newval
USING NEW;
END;

Accessing current row values in a trigger

I created a function will insert a value into a json for each row inserted in a table.
But when I execute the insert, it says me that the column doesn't exist.
Here is the function :
CREATE OR REPLACE FUNCTION insert_id_function()
RETURNS trigger AS'
BEGIN
NEW.previewcontent = previewcontent || ''{"id":1}'';
RETURN NEW;
END;'
LANGUAGE plpgsql VOLATILE;
Here is the trigger :
CREATE TRIGGER insert_id_trigger
BEFORE INSERT
ON "Telnet"
FOR EACH ROW
EXECUTE PROCEDURE insert_id_function();
Here is the error :
`ERROR: column "previewcontent" does not exist LINE 1: SELECT previewcontent || '{"id":1}'`
Here is my table definition :
This is a column of the NEW record:
NEW.previewcontent = NEW.previewcontent || ''{"id":1}'';
If the type of previewcontent is jsonb use jsonb_build_object() to add the current value of id to the jsonb column:
CREATE OR REPLACE FUNCTION insert_id_function()
RETURNS trigger AS $$
BEGIN
NEW.previewcontent = NEW.previewcontent || jsonb_build_object('id', NEW.id);
RETURN NEW;
END;
$$ LANGUAGE plpgsql VOLATILE;

How can I refer to a variable in postgresql dynamic SQL?

I'm trying to write a PostgreSQL function for table upserts that can be used for any table. My starting point is taken from a concrete function for a specific table type:
CREATE TABLE doodad(id BIGINT PRIMARY KEY, data JSON);
CREATE OR REPLACE FUNCTION upsert_doodad(d doodad) RETURNS VOID AS
$BODY$
BEGIN
LOOP
UPDATE doodad
SET id = (d).id, data = (d).data
WHERE id = (d).id;
IF found THEN
RETURN;
END IF;
-- does not exist, or was just deleted.
BEGIN
INSERT INTO doodad SELECT d.*;
RETURN;
EXCEPTION when UNIQUE_VIOLATION THEN
-- do nothing, and loop to try the update again
END;
END LOOP;
END;
$BODY$
LANGUAGE plpgsql;
The dynamic SQL version of this for any table that I've come up with is here:
SQL Fiddle
CREATE OR REPLACE FUNCTION upsert(target ANYELEMENT) RETURNS VOID AS
$$
DECLARE
attr_name NAME;
col TEXT;
selectors TEXT[];
setters TEXT[];
update_stmt TEXT;
insert_stmt TEXT;
BEGIN
FOR attr_name IN SELECT a.attname
FROM pg_index i
JOIN pg_attribute a ON a.attrelid = i.indrelid
AND a.attnum = ANY(i.indkey)
WHERE i.indrelid = format_type(pg_typeof(target), NULL)::regclass
AND i.indisprimary
LOOP
selectors := array_append(selectors, format('%1$s = target.%1$s', attr_name));
END LOOP;
FOR col IN SELECT json_object_keys(row_to_json(target))
LOOP
setters := array_append(setters, format('%1$s = (target).%1$s', col));
END LOOP;
update_stmt := format(
'UPDATE %s SET %s WHERE %s',
pg_typeof(target),
array_to_string(setters, ', '),
array_to_string(selectors, ' AND ')
);
insert_stmt := format('INSERT INTO %s SELECT (target).*', pg_typeof(target));
LOOP
EXECUTE update_stmt;
IF found THEN
RETURN;
END IF;
BEGIN
EXECUTE insert_stmt;
RETURN;
EXCEPTION when UNIQUE_VIOLATION THEN
-- do nothing
END;
END LOOP;
END;
$$
LANGUAGE plpgsql;
When I attempt to use this function, I get an error:
SELECT * FROM upsert(ROW(1,'{}')::doodad);
ERROR: column "target" does not exist: SELECT * FROM upsert(ROW(1,'{}')::doodad)
I tried changing the upsert statement to use placeholders, but I can't figure out how to invoke it using the record:
EXECUTE update_stmt USING target;
ERROR: there is no parameter $2: SELECT * FROM upsert(ROW(1,'{}')::doodad)
EXECUTE update_stmt USING target.*;
ERROR: query "SELECT target.*" returned 2 columns: SELECT * FROM upsert(ROW(1,'{}')::doodad)
I feel really close to a solution, but I can't figure out the syntax issues.
Short answer: you can't.
Variable substitution does not happen in the command string given to EXECUTE or one of its variants. If you need to insert a varying value into such a command, do so as part of constructing the string value, or use USING, as illustrated in Section 40.5.4. 1
Longer answer:
SQL statements and expressions within a PL/pgSQL function can refer to variables and parameters of the function. Behind the scenes, PL/pgSQL substitutes query parameters for such references. 2
This was the first important piece to the puzzle: PL/pgSQL does magic transformations on function parameters that turn them into variable substitutions.
The second was that fields of variable substitutions can referenced:
Parameters to a function can be composite types (complete table rows). In that case, the corresponding identifier $n will be a row variable, and fields can be selected from it, for example $1.user_id. 3
This excerpt confused me, because it referred to function parameters, but knowing that function parameters are implemented as variable substitutions under the hood, it seemed that I should be able to use the same syntax in EXECUTE.
These two facts unlocked the solution: use the ROW variable in the USING clause, and dereference its fields in the dynamic SQL. The results (SQL Fiddle):
CREATE OR REPLACE FUNCTION upsert(v_target ANYELEMENT)
RETURNS SETOF ANYELEMENT AS
$$
DECLARE
v_target_name TEXT;
v_attr_name NAME;
v_selectors TEXT[];
v_colname TEXT;
v_setters TEXT[];
v_update_stmt TEXT;
v_insert_stmt TEXT;
v_temp RECORD;
BEGIN
v_target_name := format_type(pg_typeof(v_target), NULL);
FOR v_attr_name IN SELECT a.attname
FROM pg_index i
JOIN pg_attribute a ON a.attrelid = i.indrelid
AND a.attnum = ANY(i.indkey)
WHERE i.indrelid = v_target_name::regclass
AND i.indisprimary
LOOP
v_selectors := array_append(v_selectors, format('t.%1$I = $1.%1$I', v_attr_name));
END LOOP;
FOR v_colname IN SELECT json_object_keys(row_to_json(v_target))
LOOP
v_setters := array_append(v_setters, format('%1$I = $1.%1$I', v_colname));
END LOOP;
v_update_stmt := format(
'UPDATE %I t SET %s WHERE %s RETURNING t.*',
v_target_name,
array_to_string(v_setters, ','),
array_to_string(v_selectors, ' AND ')
);
v_insert_stmt = format('INSERT INTO %I SELECT $1.*', v_target_name);
LOOP
EXECUTE v_update_stmt INTO v_temp USING v_target;
IF v_temp IS NOT NULL THEN
EXIT;
END IF;
BEGIN
EXECUTE v_insert_stmt USING v_target;
EXIT;
EXCEPTION when UNIQUE_VIOLATION THEN
-- do nothing
END;
END LOOP;
RETURN QUERY SELECT v_target.*;
END;
$$
LANGUAGE plpgsql;
For writeable CTE fans, this is trivially convertible to CTE form:
v_cte_stmt = format(
'WITH up as (%s) %s WHERE NOT EXISTS (SELECT 1 from up t WHERE %s)',
v_update_stmt,
v_insert_stmt,
array_to_string(v_selectors, ' AND '));
LOOP
BEGIN
EXECUTE v_cte_stmt USING v_target;
EXIT;
EXCEPTION when UNIQUE_VIOLATION THEN
-- do nothing
END;
END LOOP;
RETURN QUERY SELECT v_target.*;
NB: I have done zero performance testing on this solution, and I am relying on the analysis of others for its correctness. For now it appears to run correctly on PostgreSQL 9.3 in my development environment. YMMV.

How to iterate over a record when the columns are dynamic

I have this function in postgres which takes PVH_COLS_DYNA that contains the columns that are going in to the query:
CREATE OR REPLACE FUNCTION DRYNAMIC_DATA_F(PVH_COLS_DYNA VARCHAR) RETURNS numeric AS $$
DECLARE
VV_QUERY_DINAMIC VARCHAR;
VV_ROW_RECORD record;
BEGIN
VV_QUERY_DINAMIC:=' SELECT '|| PVH_COLS_DYNA ||' FROM as_detalle_carga WHERE fk_id_carga_cartera = 1234 ;';
FOR VV_ROW_RECORD IN EXECUTE VV_QUERY_DINAMIC LOOP
raise notice ' data % ', VV_ROW_RECORD.???????;
END LOOP;
return 1;
END;
$$ LANGUAGE plpgsql;
How can I get the data from the record variable VV_ROW_RECORD, since the columns are dynamic?
VV_ROW_RECORD.1
VV_ROW_RECORD.?1
VV_ROW_RECORD.[1]
VV_ROW_RECORD.?????
You cannot reference columns like array items, columns have to be referenced by name.
The dynamic part is not getting the row in your example, but referencing each column.
CREATE OR REPLACE FUNCTION dynamic_data_f(pvh_cols_dyna text)
RETURNS numeric AS
$func$
DECLARE
_row as_detalle_carga%ROWTYPE;
_col text;
_data text;
BEGIN
SELECT *
INTO _row
FROM as_detalle_carga
WHERE fk_id_carga_cartera = 1234;
FOREACH _col IN ARRAY string_to_array(pvh_cols_dyna, ',')
LOOP
EXECUTE format('SELECT ($1).%I::text', trim(_col))
USING _row
INTO _data;
RAISE NOTICE 'data: % ', _data;
END LOOP;
RETURN 1;
END
$func$ LANGUAGE plpgsql;
%I is an argument to format(), properly escaping identifiers as needed.
$1 in the query string for EXECUTE is a parameter filled in by the USING clause (not to be confused with function parameters!).
Related answers (with more explanation):
Postgres pl/pgsql ERROR: column "column_name" does not exist
Iterating over integer[] in PL/pgSQL
How to use EXECUTE FORMAT ... USING in postgres function
You cannot iterate record columns directly. You have to convert it first into something iterable, like json or hstore.
FOR vv_row_record IN EXECUTE vv_query_dynamic LOOP
FOR vv_row_record_pairs IN SELECT * FROM json_each(row_to_json(vv_row_record)) LOOP
RAISE NOTICE ' field "%" in json is % ',
vv_row_record_pairs.key,
vv_row_record_pairs.value;
END LOOP;
-- OR
FOR vv_row_record_pairs IN SELECT * FROM each(hstore(vv_row_record)) LOOP
RAISE NOTICE ' field "%" in text representation is % ',
vv_row_record_pairs.key,
vv_row_record_pairs.value;
END LOOP;
END LOOP;