PostgreSQL: compare NULL and normal value - postgresql

I have a trigger in PostgreSQL
CREATE OR REPLACE FUNCTION table_update_func_pk1() RETURNS trigger AS $$
DECLARE
ri RECORD;
old_value TEXT;
new_value TEXT;
BEGIN
FOR ri IN
SELECT column_name FROM information_schema.columns
WHERE
table_schema = quote_ident('public')
AND table_name = quote_ident(TG_TABLE_NAME)
ORDER BY ordinal_position
LOOP
EXECUTE 'SELECT ($1).' || ri.column_name || '::text' INTO new_value USING NEW;
EXECUTE 'SELECT ($1).' || ri.column_name || '::text' INTO old_value USING OLD;
IF new_value <> old_value AND ri.column_name != 'update_by' THEN
INSERT INTO protokoll(datetime, operation, tabelle, field, pk1, old_value, new_value, update_by)
VALUES(now(), TG_OP, TG_TABLE_NAME, ri.column_name, NEW.cfg, old_value, new_value, NEW.update_by);
END IF;
END LOOP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
Sometimes if old_value is changed from NULL to normal value (or from normal value to NULL), the condition "new_value <> old_value" is NOT true but unknown. I would like ask, there is a method that I can get true in the case. Thanks.

Use is distinct from:
if new_value IS DISTINCT FROM old_value and ri.column_name <> 'update_by' then
...
end if;

Related

SQL Error [42601]: ERROR: syntax error at or near "("

I am trying to call this function
CREATE or replace FUNCTION get_column_names(param text)
RETURNS text AS $get_column_names$
DECLARE
return_value text;
x record;
y int;
begin
return_value := '';
y := 0;
for x in SELECT *
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = param
ORDER BY ordinal_position
loop
if (y = 0)
then
return_value = x.column_name;
else
return_value := return_value ||','|| x.column_name;
end if;
Y = Y+1;
end loop;
return return_value;
END;
$get_column_names$ LANGUAGE plpgsql;
Which this function works here
select get_column_names('users');
Results
first_name,middle_name,last_name,gender,locale,auth_user_id,identity_id,active,date_created,provisioned,user_id,timezone,last_seen
However when I use it in another function I get an error.
Here is the other function I am calling it from.
CREATE OR REPLACE FUNCTION process_users_audit() RETURNS TRIGGER AS $users_audit$
BEGIN
--
-- Create a row in users_audit to reflect the operation performed on users
--
IF (TG_OP = 'DELETE') THEN
INSERT INTO users_audit (user_audit_id, stamp, operation, db_user, get_column_names(TG_TABLE_NAME)) values (uuid_generate_v4(), now(), 'D', user, OLD.*);
RETURN OLD;
ELSIF (TG_OP = 'UPDATE') THEN
INSERT INTO users_audit (user_audit_id, stamp, operation, db_user, get_column_names(TG_TABLE_NAME)) values (uuid_generate_v4(), now(), 'U', user, NEW.*);
RETURN NEW;
ELSIF (TG_OP = 'INSERT') THEN
INSERT INTO users_audit (user_audit_id, stamp, operation, db_user, get_column_names(TG_TABLE_NAME)) values (uuid_generate_v4(), now(), 'I', user, NEW.*);
RETURN NEW;
END IF;
RETURN NULL; -- result is ignored since this is an AFTER trigger
END;
$users_audit$ LANGUAGE plpgsql;
The error I am receiving is this (same as title).
SQL Error [42601]: ERROR: syntax error at or near "("
Position: 406
UPDATE
#Ed Brook's answer worked except for had to change how NEW was being used. Will remove this update once he has updated his answer but here is what worked.
CREATE OR REPLACE FUNCTION process_audit_table() RETURNS TRIGGER AS $audit_table$
BEGIN
--
-- Create a row in the requesting table's audit to reflect the operation performed on users,
-- make use of the special variable TG_OP to work out the operation.
--
IF (TG_OP = 'DELETE') then
EXECUTE 'INSERT INTO ' || concat(TG_TABLE_NAME, '_audit') || ' (' || get_primary_key_name(concat(TG_TABLE_NAME, '_audit')) || ', stamp, operation, db_user, ' || get_column_names(TG_TABLE_NAME) || ') values (uuid_generate_v4(), now(), ''D'', user, $1.*);'
USING OLD;
ELSIF (TG_OP = 'UPDATE') then
EXECUTE 'INSERT INTO ' || concat(TG_TABLE_NAME, '_audit') || ' (' || get_primary_key_name(concat(TG_TABLE_NAME, '_audit')) || ', stamp, operation, db_user, ' || get_column_names(TG_TABLE_NAME) || ') values (uuid_generate_v4(), now(), ''U'', user, $1.*);'
USING NEW;
ELSIF (TG_OP = 'INSERT') then
EXECUTE 'INSERT INTO ' || concat(TG_TABLE_NAME, '_audit') || ' (' || get_primary_key_name(concat(TG_TABLE_NAME, '_audit')) || ', stamp, operation, db_user, ' || get_column_names(TG_TABLE_NAME) || ') values (uuid_generate_v4(), now(), ''I'', user, $1.*);'
USING NEW;
END IF;
RETURN NULL; -- result is ignored since this is an AFTER trigger
END;
$audit_table$ LANGUAGE plpgsql;
Also added some extra bits in there like dynamically getting the audit table name and then get_primary_key_name below is that function.
CREATE OR REPLACE FUNCTION get_primary_key_name(table_name text)
RETURNS text AS $primary_key_name$
DECLARE
return_value text;
BEGIN
SELECT pg_attribute.attname INTO return_value
FROM pg_index, pg_class, pg_attribute, pg_namespace
WHERE pg_class.oid = table_name::regclass
AND indrelid = pg_class.oid
AND nspname = 'public'
AND pg_class.relnamespace = pg_namespace.oid
AND pg_attribute.attrelid = pg_class.oid
AND pg_attribute.attnum = any(pg_index.indkey)
AND indisprimary;
RETURN return_value;
END
$primary_key_name$ LANGUAGE plpgsql;
You may need to use EXECUTE, as I don't think you can have function calls in the column list?
ref: https://www.postgresql.org/docs/current/plpgsql-statements.html#PLPGSQL-STATEMENTS-EXECUTING-DYN
So, something like this?
EXECUTE 'INSERT INTO users_audit (user_audit_id, stamp, operation, db_user, ' || get_column_names(TG_TABLE_NAME) || ') values (uuid_generate_v4(), now(), ''U'', user, NEW.*)';

postgresql update true if other table column having value else false

I want to update tableA column as "True" if in tableB column having any value other wise update as false.
The updating process should done for all schema which is having this tables dynamically.
Please help me on this.
update TA set TA.col=true else false (if TB.col having any value)
DO $$
declare
l_rec record;
l_schema_name varchar(100);
l_sql varchar(100);
BEGIN
for rec in select Table_schema, table_name from information_schema.tables where table_name='table A' and 'table B'
LOOP
l_schema_name :=rec.Table_schema;
l_sql := 'update '||l_schema_name||'.TA Set TA.column1= (case when isnull(TB.Column,' ')=' ' then false else true end) From tableA TA left join tableB TB on TA.Col=TB.Col';
begin
execute l_sql;
end;
END loop;
END;
$$;
i have a solution for the above query.
DO $$
declare
rec record;
l_schema_name varchar(100);
l_sql varchar(1000);
BEGIN
for rec in select Table_schema, table_name from information_schema.tables
where table_name='TA' and table_schema='schema_name'
LOOP
l_schema_name :=rec.Table_schema;
l_sql := 'update '||l_schema_name||'.TableA TA set TA.col=true
where id in (select id from '||l_schema_name||'.Tableb TB)';
begin
execute l_sql;
Raise notice 'Updated for % :',l_schema_name;
end;
END loop;
END;
$$;

How to use a Function Parameter in a Cursor that's incorporated with Dynamic SQL in Postgres Functions?

Created this Postgres Function which is working fine, but the actual requirement is to pass the input parameter in the function to the Cursor which uses the dynamic SQL as follows,
The below is the Function
CREATE OR REPLACE FUNCTION ssp2_pcat.find_shift_dates (date_to_find date)
RETURNS void
LANGUAGE 'plpgsql'
COST 100
VOLATILE
AS $BODY$
DECLARE
C1 CURSOR FOR
SELECT TABLE_NAME, 'SELECT COUNT(*) FROM ' || TABLE_NAME || ' WHERE ' ||
COLUMN_NAME || ' = '||
'CASE WHEN ' || COLUMN_NAME || ' LIKE ' || '''%START%'''||' THEN
date_to_find ELSE date_to_find-1 END;' SQL_TEXT
FROM (
SELECT TABLE_NAME, COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME IN (SELECT TABLE_NAME FROM RESET_DATES WHERE RESET_IT =
'Y') AND
UPPER(DATA_TYPE) = 'DATE'
AND (COLUMN_NAME LIKE '%START%' OR COLUMN_NAME LIKE '%END%')
AND (COLUMN_NAME NOT LIKE '%TEST%'
AND COLUMN_NAME NOT LIKE '%PCAT%'
AND COLUMN_NAME NOT LIKE '%ORDER%'
AND COLUMN_NAME NOT LIKE '%SEASON%'
AND COLUMN_NAME NOT LIKE '%_AT')
ORDER BY 1, 2) A;
END_COUNT INTEGER := 0;
START_COUNT INTEGER := 0;
TABLENAME VARCHAR(32) := 'ALFU';
l_start TIMESTAMP;
l_end TIMESTAMP;
Time_Taken VARCHAR(20);
BEGIN
l_start := clock_timestamp();
DELETE FROM SHIFT_DATES_COUNT;
FOR I IN C1 LOOP
IF I.TABLE_NAME <> TABLENAME THEN
INSERT INTO SHIFT_DATES_COUNT VALUES (TABLENAME, START_COUNT,
END_COUNT, current_timestamp::timestamp(0));
TABLENAME := I.TABLE_NAME;
END_COUNT := 0;
START_COUNT := 0;
END IF;
IF STRPOS(I.SQL_TEXT, 'END') > 0 THEN
EXECUTE I.SQL_TEXT INTO END_COUNT;
RAISE NOTICE '% ', ('END: ' || I.SQL_TEXT);
ELSE
EXECUTE I.SQL_TEXT INTO START_COUNT;
RAISE NOTICE '% ', ('START: ' || I.SQL_TEXT);
END IF;
END LOOP;
INSERT INTO SHIFT_DATES_COUNT VALUES (TABLENAME, START_COUNT, END_COUNT,
current_timestamp::timestamp(0));
RAISE NOTICE '% ', ('INSERT INTO SHIFT_DATES_COUNT Done...');
l_end := clock_timestamp();
Time_Taken := (l_end-l_start);
RAISE NOTICE '% ', ('FIND_SHIFT_DATES Took: ' || Time_Taken );
END;
$BODY$;
Please let me know how can I use the date_to_find input parameter in the Dynamic SQL in the Cursor in the above Function.
You can use unbound cursor, clause fetch to get data from cursor, and exit when not found to finish, like:
CREATE OR REPLACE FUNCTION example (p_name text) RETURNS void LANGUAGE 'plpgsql' AS $$
DECLARE
C1 refcursor;
res record;
BEGIN
OPEN c1 FOR EXECUTE 'SELECT * FROM pg_database WHERE datname like ''%'||p_name||'%''';
LOOP
FETCH c1 INTO res;
EXIT WHEN not found;
raise notice 'value datname: %',res.datname;
END LOOP;
CLOSE c1;
RETURN;
END; $$;
--in my case
select example ('test')
NOTICE: value datname: test
NOTICE: value datname: test_msmov
NOTICE: value datname: test_resources
NOTICE: value datname: test_load_table
NOTICE: value datname: test_resources2
Total query runtime: 63 msec
1 row retrieved.
You can use EXECUTE clause for open cursor, see the documentation of PostgreSQL
https://www.postgresql.org/docs/10/plpgsql-cursors.html#PLPGSQL-CURSOR-OPENING
Example:
OPEN curs1 FOR EXECUTE format('SELECT * FROM %I WHERE col1 = $1',tabname) USING keyvalue;

PostgreSQL log trigger optimalization

I spent a lot of time trying to optimize our pgsql log trigger which started to be a problem. I did huge progress (from 18min to 2.5min by inserting 3M rows) but I would like to know if some pgSql masters will be able to do it even better.
CREATE OR REPLACE FUNCTION table_log_trig()
RETURNS trigger AS
$BODY$
DECLARE
col TEXT; -- Single column name to save
newVal TEXT; -- New value for column
oldVal TEXT; -- Old value for column
colLimit TEXT[]; -- Columns that should be logged
BEGIN
IF TG_ARGV[0] IS NOT NULL THEN
-- Trigger specifies columns to log
SELECT array_agg(unnest)
FROM unnest(string_to_array(TG_ARGV[0], ','))
INTO colLimit;
ELSE
-- Trigger with no params. Log all columns
SELECT array_agg(json_object_keys)
FROM json_object_keys(row_to_json(NEW))
WHERE json_object_keys NOT IN ('id', 'created_at', 'updated_at') -- Exceptions
INTO colLimit;
END IF;
-- Loop over columns that should be saved in log
FOREACH col IN ARRAY colLimit
LOOP
-- INSERT & UPDATE
EXECUTE 'SELECT ($1).' || col || '::text' INTO newVal USING NEW;
-- UPDATE
IF TG_OP = 'UPDATE' THEN
EXECUTE 'SELECT ($1).' || col || '::text' INTO oldVal USING OLD;
END iF;
-- Add only new or changed data
IF
newVal != oldVal OR
(oldVal IS NULL AND newVal IS NOT NULL) OR
(oldVal IS NOT NULL AND newVal IS NULL)
THEN
INSERT INTO tab_logs (record_id, field_name, old_value, new_value, created_at, created_by, action)
VALUES (NEW.id, col, oldVal, newVal, NOW(), 999, 'O');
END IF;
END LOOP;
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
row_to_json() returns both column names and values; you may as well make use of these values, rather than extracting them later via dynamic SQL.
I haven't thoroughly tested this, let alone benchmarked it, but here's the gist of it:
CREATE OR REPLACE FUNCTION table_log_trig() RETURNS trigger AS
$$
DECLARE
OldJson JSONB = NULL;
BEGIN
IF TG_OP <> 'INSERT' THEN
OldJson := to_jsonb(old);
END IF;
INSERT INTO tab_logs (record_id, field_name, old_value, new_value, created_at, created_by, action)
SELECT new.id, key, OldValues.value, NewValues.value, now(), 999, 'O'
FROM jsonb_each(to_jsonb(new)) NewValues
LEFT JOIN jsonb_each(OldJson) OldValues USING (key)
WHERE
(
(TG_ARGV[0] IS NULL AND key NOT IN ('id', 'created_at', 'updated_at')) OR
(TG_ARGV[0] IS NOT NULL AND key = ANY(string_to_array(TG_ARGV[0], ',')))
) AND
OldValues.value::text IS DISTINCT FROM NewValues.value::text;
RETURN NULL;
END
$$
LANGUAGE plpgsql VOLATILE;

after update the table insert data like arrays

I have two tables
Table A
col1 col2 col3
Table B
table_name column_name new_value old_value
if any update happened on table A it will insert the data on table B
the output of table B is==>
table_name column_name new_value old_value
---------------- ------------------ -------------- -----------
A {col1}
A {col1,col2} {col1.new_value, {col1.old_value,
col2.new_value} col2.old_value},
so anyone can tell me how to capture the column_names and it store data target table like arrays
Try this
Use Trigger Function
CREATE OR REPLACE FUNCTION update_history()
RETURNS trigger AS
$BODY$
DECLARE col_name VARCHAR[];
DECLARE od_value VARCHAR[];
DECLARE ne_value VARCHAR[];
DECLARE each_column RECORD;
DECLARE each_entity RECORD;
DECLARE column_name VARCHAR;
DECLARE old_value VARCHAR;
DECLARE new_value VARCHAR;
FOR each_column IN
select c.column_name --- Get the all column names in affected table
from information_schema.columns c
where(table_name = tg_relname And c.TABLE_SCHEMA = TG_TABLE_SCHEMA)
LOOP
FOR each_entity IN --- Its used to get old and new columns value
EXECUTE 'SELECT text((' || quote_literal(OLD.*) || '::"' || tg_table_schema || '"."' || tg_relname || '")."' || each_column.column_name || '") as old_val,
text((' || quote_literal(NEW.*) || '::"' || tg_table_schema || '"."' || tg_relname || '")."' || each_column.column_name || '")
AS new_val
FROM "' || tg_table_schema || '"."' || tg_relname || '";'
LOOP
old_value = each_entity.old_val;
new_value = each_entity.new_val;
IF old_value != new_value THEN
i=i+1;
col_name[i]=each_column.column_name;
od_value[i]=old_value;
ne_value[i]=new_value;
END IF;
END LOOP;
END LOOP;
INSERT INTO B
(
tablename,
columnnames,
oldvalues,
newvalues
)
VALUES
(
tg_relname,
col_name,
od_value,
ne_value
);
End if;
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
I thinks it's a good case to use hstore extension of PostgreSQL:
create or replace function history_trigger_func()
returns trigger AS
$$
begin
insert into TableB
select
tg_relname,
case when tg_op in ('UPDATE', 'INSERT') then hstore(new) end,
case when tg_op in ('UPDATE', 'DELETE') then hstore(old) end;
return null;
end;
$$
language plpgsql;
create trigger tr_history_trigger after insert or update or delete on TableA
for each row execute procedure history_trigger_func();
sql fiddle demo
You can extend this further by removing columns which are not changing, or, if you're using PostgreSQL 9.3, you can use JSON instead of hstore.