I am trying to create function in postgres
CREATE OR REPLACE FUNCTION cms_inventory_proc()
RETURNS TRIGGER
AS
$BODY$
BEGIN
IF NEW.permanent = 'YES' THEN
INSERT INTO cms_inventory(id, version, scheme_id, register_num, cms_reference, sensor_id)
VALUES (NEW.id, 0, NEW.scheme_id, NEW.register_num, NEW.cms_reference, NEW.sensor_id);
END IF;
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql;
ERROR: syntax error at or near "IF"
LINE 9: IF NEW.permanent ='YES' THEN
While it is working in online compiler
I've looked up pretty much everything I could find regarding this issue, but I still don't understand what is wrong with this trigger:
CREATE OR REPLACE FUNCTION func_SubstitutePostLatLng_Upt()
RETURNS trigger AS
$BODY$
BEGIN
IF OLD.post_latlng IS NULL AND NEW.post_latlng IS NULL AND NEW.place_guid IS NOT NULL THEN
raise notice 'SELECT';
SELECT place.geom_center, place.city_guid
INTO NEW.post_latlng, NEW.city_guid
FROM public.place
WHERE (place.origin_id, place.place_guid) IN (VALUES (NEW.origin_id,NEW.place_guid));
raise notice 'Value db_geom: %', NEW.post_latlng;
raise notice 'Value db_city_guid: %', NEW.city_guid;
IF NEW.post_latlng IS NOT NULL THEN
NEW.post_geoaccuracy = 'place';
IF NEW.city_guid IS NOT NULL THEN
SELECT country_guid INTO NEW.country_guid
FROM public.city WHERE (origin_id, city_guid) IN (VALUES (NEW.origin_id,NEW.city_guid));
END IF;
END IF;
END IF;
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
DROP TRIGGER IF EXISTS trig_SubstitutePostLatLng_Upd on public.post;
CREATE TRIGGER trig_SubstitutePostLatLng_Upd
BEFORE UPDATE
ON public.post
FOR EACH ROW
WHEN (pg_trigger_depth() < 1)
EXECUTE PROCEDURE func_SubstitutePostLatLng_Upt()
(I have a second similar trigger for insert)
The code is supposed to do the following:
On Update on table "post", check if no post_latlng is submitted (=NULL), and if yes, substitute post_latlng from table place (geom_center), if available.
However, no matter what I do, I get the following when updating an entry in table "post" (=triggering the above trigger):
NOTICE: SELECT
NOTICE: Value db_geom: <NULL>
NOTICE: Value db_city_guid: <NULL>
INSERT 0 1
Query returned successfully in 47 msec.
The test-data for place_guid, geom_center etc. is definitely available and both
raise notice 'Value db_geom: %', NEW.post_latlng;
raise notice 'Value db_city_guid: %', NEW.city_guid;
should not output NULL.
There were several smaller issues, it now works. Here is a more cleaner code that uses variables in between:
CREATE OR REPLACE FUNCTION func_SubstitutePostLatLng_Upt()
RETURNS trigger AS
$BODY$
DECLARE
db_geom_center text;
db_city_guid text;
db_country_guid text;
BEGIN
IF OLD.post_latlng IS NULL AND NEW.post_latlng IS NULL AND NEW.place_guid IS NOT NULL THEN
SELECT place.geom_center, place.city_guid
INTO db_geom_center, db_city_guid
FROM public.place
WHERE (place.origin_id, place.place_guid) IN (VALUES (NEW.origin_id,NEW.place_guid));
IF db_geom_center IS NOT NULL THEN
NEW.post_latlng = db_geom_center;
NEW.post_geoaccuracy = 'place';
END IF;
IF db_city_guid IS NOT NULL THEN
NEW.city_guid = db_city_guid;
SELECT city.country_guid
INTO db_country_guid
FROM public.city
WHERE (city.origin_id, city.city_guid) IN (VALUES (NEW.origin_id,db_city_guid));
NEW.country_guid = db_country_guid;
END IF;
END IF;
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
I've written 3 functions to log transactions to designated tables:
CREATE OR REPLACE FUNCTION log_sites() RETURNS TRIGGER AS $body$
DECLARE
target_row sites%ROWTYPE;
BEGIN
IF (TG_OP = 'DELETE') THEN
-- No NEW row
target_row = OLD;
ELSE
target_row = NEW;
END IF;
INSERT INTO sites_history (transaction_type,
transaction_time,
site_id,
address,
name,
shared_key)
VALUES (TG_OP,
NOW(),
target_row.site_id,
target_row.address,
target_row.name,
target_row.shared_key);
RETURN target_row;
END;
$body$
LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION log_licenses() RETURNS TRIGGER AS $body$
DECLARE
target_row licenses%ROWTYPE;
BEGIN
IF (TG_OP = 'DELETE') THEN
target_row = OLD;
ELSE
target_row = NEW;
END IF;
INSERT INTO licenses_history (transaction_type,
transaction_time,
license_id,
start_date,
expiration_date,
site_id)
VALUES (TG_OP,
NOW(),
target_row.license_id,
target_row.start_date,
target_row.expiration_date,
target_row.site_id);
RETURN target_row;
END;
$body$
LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION log_clients() RETURNS TRIGGER AS $body$
DECLARE
target_row clients%ROWTYPE;
BEGIN
IF (TG_OP = 'DELETE') THEN
target_row = OLD;
ELSE
target_row = NEW;
END IF;
INSERT INTO clients_history (transaction_type,
transaction_time,
mac_address,
hardware,
license_id,
site_id)
VALUES (TG_OP,
NOW(),
target_row.mac_address,
target_row.hardware,
target_row.license_id,
target_row.site_id);
RETURN target_row;
END;
$body$
LANGUAGE plpgsql;
This results in a big ugly block of PL/pgSQL, which nobody at my job is especially familiar with, myself included. A coworker suggested it'd be nice to consolidate/DRYify all this stuff, but for the life of me, I'm not sure how, especially considering each table needs a separate trigger, and the way triggers pass data to their functions. Any suggestions?
ETA:
1) Here are the triggers:
CREATE TRIGGER sites_log
AFTER INSERT OR UPDATE OR DELETE
ON sites
FOR EACH ROW EXECUTE PROCEDURE log_transactions();
CREATE TRIGGER licenses_log
AFTER INSERT OR UPDATE OR DELETE
ON licenses
FOR EACH ROW EXECUTE PROCEDURE log_transactions();
CREATE TRIGGER clients_log
AFTER INSERT OR UPDATE OR DELETE
ON clients
FOR EACH ROW EXECUTE PROCEDURE log_transactions();
Here's what I've got now, after quite a bit of messing around:
CREATE OR REPLACE FUNCTION log_transactions() RETURNS TRIGGER LANGUAGE plpgsql AS $body$
DECLARE
target_row RECORD;
target_cols text[];
col_name RECORD;
col_name_str text;
right_now timestamp without time zone;
q_str text;
BEGIN
right_now := now();
target_cols := '{}';
FOR col_name IN SELECT column_name::text FROM information_schema.columns WHERE table_name = TG_TABLE_NAME AND table_schema = TG_TABLE_SCHEMA LOOP
col_name_str := col_name.column_name::text;
target_cols = ARRAY_APPEND(target_cols, col_name_str);
END LOOP;
RAISE NOTICE 'target_cols: %', target_cols;
IF (TG_OP = 'DELETE') THEN
target_row := OLD;
ELSE
target_row := NEW;
END IF;
RAISE NOTICE 'target_row: %', target_row;
EXECUTE format('INSERT INTO %I_history (transaction_time, transaction_type) VALUES (%L, %L)', TG_TABLE_NAME, right_now, TG_OP);
q_str := format('UPDATE %I_history SET (%s) = ', TG_TABLE_NAME, array_to_string(target_cols, ', ')) || '$1' || format(' WHERE transaction_type = %L AND transaction_time = %L', TG_OP, right_now);
EXECUTE q_str USING target_row;
RETURN target_row;
END;
$body$;
This doesn't work either, and it's spiraling out of control, complexity-wise.
Personally, I use a set of home-grown functions Below for auditing any table I want. All I do is run audit.enable() on any table I want to audit and it's stored in the general tables I have here. Yes, it's not exactly what you're doing, but it's the most "DRY" think there is really, I do it once and never again-- ever.
CREATE TABLE audit.audit_log
(
audit_log_seq serial primary key,
schema_name text NOT NULL,
table_name text NOT NULL,
db_username text,
user_seq bigint,
logged_ip inet,
log_timestamp TIMESTAMP NOT NULL DEFAULT (now()),
action TEXT NOT NULL CHECK (action IN ('I','D','U')),
comment varchar(500),
old_data hstore,
new_data hstore,
query text
) WITH
(
fillfactor=100
);
CREATE INDEX audit_log_schema_table_idx ON audit.audit_log(schema_name,table_name);
CREATE INDEX audit_log_timestamp_utc_idx ON audit.audit_log(log_timestamp);
CREATE INDEX audit_log_uname on audit.audit_log(user_seq);
-- generic function for all tables
CREATE OR REPLACE FUNCTION audit.log_func() RETURNS TRIGGER AS $body$
DECLARE
v_old_data hstore;
v_new_data hstore;
v_query text;
v_comment varchar;
-- v_old_data TEXT;
-- v_new_data TEXT;
BEGIN
v_query=current_query();
IF (TG_OP = 'UPDATE') THEN
v_old_data := hstore(OLD.*);
v_new_data := hstore(NEW.*);
v_comment=v_new_data -> 'audit_comment';
ELSIF (TG_OP = 'DELETE') THEN
v_old_data := hstore(OLD.*);
ELSIF (TG_OP = 'INSERT') THEN
v_new_data := hstore(NEW.*);
v_comment=v_new_data -> 'audit_comment';
ELSE
RAISE WARNING '[audit.log_func] - Other action occurred: %, at %',TG_OP,now();
RETURN NULL;
END IF;
INSERT INTO audit.audit_log (schema_name,table_name,db_username,user_seq,logged_ip,action,old_data,new_data,query, comment)
VALUES (TG_TABLE_SCHEMA::TEXT,
TG_TABLE_NAME::TEXT,
session_user::TEXT,
coalesce(current_setting('mvc.user_seq'),'0')::bigint, --current user
inet_client_addr(),
substring(TG_OP,1,1),
v_old_data,
v_new_data,
v_query,
v_comment);
IF (TG_OP = 'DELETE') THEN
RETURN OLD;
END IF;
RETURN NEW;
EXCEPTION
WHEN data_exception THEN
RAISE WARNING '[audit.log_func] - UDF ERROR [DATA EXCEPTION] - SQLSTATE: %, SQLERRM: %',SQLSTATE,SQLERRM;
RETURN NULL;
WHEN unique_violation THEN
RAISE WARNING '[audit.log_func] - UDF ERROR [UNIQUE] - SQLSTATE: %, SQLERRM: %',SQLSTATE,SQLERRM;
RETURN NULL;
--WHEN OTHERS THEN
-- RAISE WARNING '[audit.log_func] - UDF ERROR [OTHER] - SQLSTATE: %, SQLERRM: %',SQLSTATE,SQLERRM;
-- RETURN NULL;
END;
$body$ language plpgsql security definer;
CREATE OR REPLACE FUNCTION audit.enable(p_table_name text,p_schema_name text DEFAULT 'dallas') RETURNS VOID as $body$
DECLARE
BEGIN
EXECUTE 'create trigger trg_audit_'||p_table_name||' BEFORE INSERT OR UPDATE OR DELETE ON '||p_schema_name||'.'||p_table_name|| ' FOR EACH ROW EXECUTE PROCEDURE audit.log_func()';
exception when duplicate_object then null;
END;
$body$ language plpgsql security definer;
I have a trigger that looks something like this:
CREATE OR REPLACE FUNCTION CHECK_SCHEDULE()
RETURNS TRIGGER AS
$BODY$
BEGIN
IF EXISTS(
SELECT DAY, TIME FROM MEETING
WHERE NEW.DAY = MEETING.DAY AND NEW.TIME > MEETING.TIME
) THEN
RAISE EXCEPTION 'THERE IS A MEETING HAPPENING ON % % ', NEW.DAY, NEW.TIME;
ELSE
RETURN NEW;
END IF;
END;
$BODY$ LANGUAGE PLPGSQL;
This works fine except I want the message to be the time it's conflicting with: There is a meeting happening on MEETING.DAY and MEETING.TIME.
However I cannot do this because it doesn't know what these variables are. Is it possible to store the values in my select clause so I can use them later?
You can move the day and time into a declared variable (e.g. a RECORD) for reference later.
CREATE OR REPLACE FUNCTION CHECK_SCHEDULE()
RETURNS TRIGGER AS
$BODY$
DECLARE
meetinginfo RECORD;
BEGIN
SELECT meeting.day, meeting.time
INTO meetinginfo
FROM meeting
WHERE new.day = meeting.day
AND new.time > meeting.time
ORDER BY new.time
LIMIT 1;
IF FOUND THEN
RAISE EXCEPTION 'THERE IS A MEETING HAPPENING ON % %', meetinginfo.day, meetinginfo.time;
END IF;
RETURN NEW;
END;
$BODY$ LANGUAGE plpgsql;
I have the following trigger function:
CREATE OR REPLACE FUNCTION update_modelname_function()
RETURNS trigger AS
$BODY$
BEGIN
IF tg_op = 'INSERT' THEN
new.model_name := upper(new.model_name);
RETURN new;
END IF;
IF tg_op = 'UPDATE' THEN
old.model_name := upper(old.model_name);
RETURN new;
END IF;
END
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
What I'm trying to achieve is for the value of the column model_name to always be uppercased when it's persisted in the table. However nothing seems to happen. Any ideas?
You accidentally updated OLD instead of NEW. Try:
CREATE OR REPLACE FUNCTION update_modelname_function()
RETURNS trigger
LANGUAGE plpgsql AS
$func$
BEGIN
IF TG_OP = 'INSERT' THEN
NEW.model_name := upper(NEW.model_name);
RETURN NEW;
ELSIF TG_OP = 'UPDATE' THEN
NEW.model_name := upper(NEW.model_name); -- !
RETURN NEW;
END IF;
END
$func$;
If the example shows the whole code, and the actual trigger(s) only fires on INSERT and/or UPDATE, further simplify:
CREATE OR REPLACE FUNCTION update_modelname_function()
RETURNS trigger
LANGUAGE plpgsql AS
$func$
BEGIN
NEW.model_name := upper(NEW.model_name);
RETURN NEW;
END
$func$;