Is there a way to declare postgresql row immutable given a condition? - postgresql

I'm creating records in a transaction.
begin;
update parent_records where id=5 set closed = now();
insert into child_records ...;
commit;
I want to prevent inserting new child_records once the parent record is closed. It seems like setting a rule on parent_records to blow up on update actions when closed would solve the problem, as the transaction would fail.
I could do the update with where closed is null then check in app code if any rows were updated and rollback, but i would rather the constraint be in the database itself.
How do i mark the parent row immutable (updates fail with error) when a condition is met (closed column not null)?

Use a trigger, e.g.:
create function before_update_on_parent_records()
returns trigger language plpgsql as $$
begin
if old.closed is not null then
raise exception 'cannot update because the row is closed';
end if;
return new;
end $$;
create trigger before_update_on_parent_records
before update on parent_records
for each row execute procedure before_update_on_parent_records();

Related

How to make a PostgreSQL constraint only apply to a new value

I'm new to PostgreSQL and really loving how constraints work with row level security, but I'm confused how to make them do what I want them to.
I have a column and I want add a constraint that creates a minimum length for a text column, this check works for that:
(length((column_name):: text) > 6)
BUT, it also then prevents users updating any rows where column_name is already under 6 characters.
I want to make it so they can't change that value TO that, but can still update a row where that is already happening, so they can change it as needed according to my new policy.
Is this possible?
BUT, it also then prevents users updating any rows where column_name is already under 6 characters.
Well, no. When you try to add that CHECK constraint, all existing rows are checked, and an exception is raised if any violation is found.
You would have to make it NOT VALID. Then yes.
You really need a trigger on INSERT or UPDATE that checks new values. Not as cheap and not as bullet-rpoof, but still pretty solid. Like:
CREATE OR REPLACE FUNCTION trg_col_min_len6()
RETURNS trigger
LANGUAGE plpgsql AS
$func$
BEGIN
IF TG_OP = 'UPDATE'
AND OLD.column_name IS NOT DISTINCT FROM NEW.column_name THEN
-- do nothing
ELSE
RAISE EXCEPTION 'New value for column "note" must have at least 6 characters.';
END IF;
RETURN NEW;
END
$func$;
-- trigger
CREATE TRIGGER tbl1_column_name_min_len6
BEFORE INSERT OR UPDATE ON tbl
FOR EACH ROW
WHEN (length(NEW.column_name) < 7)
EXECUTE FUNCTION trg_col_min_len6();
db<>fiddle here
It should be most efficient to check in a WHEN condition to the trigger directly. Then the trigger function is only ever called for short values and can be super simple.
See:
Trigger with multiple WHEN conditions
Fire trigger on update of columnA or ColumnB or ColumnC
You can create separate triggers for Insert and Update letting each completely define when it should fired. If completely different logic is required for the DML action this technique allows writing dedicated trigger functions. In this case that is not required the trigger function reduces to raise exception .... See Demo
-- Single trigger function for both Insert and Delete
create or replace function trg_col_min_len6()
returns trigger
language plpgsql
as $$
begin
raise exception 'Cannot % val = ''%''. Must have at least 6 characters.'
, tg_op, new.val;
return null;
end;
$$;
-- trigger before insert
create trigger tbl_val_min_len6_bir
before insert
on tbl
for each row
when (length(new.val) < 6)
execute function trg_col_min_len6();
-- trugger before update
create trigger tbl_val_min_len6_bur
before update
on tbl
for each row
when ( length(new.val) < 6
and new.val is distinct from old.val
)
execute function trg_col_min_len6();

PostreSQL trigger disallow an action without exceptions

Trying to solve a problem for a university assignment. Each row in the table has a date field, and I need to disallow deleting rows that have this field less than 5 years old. Using a trigger is required and it must not raise exceptions. How can I do it? I tried something like this but it doesn't work:
create or replace function no_change()
returns trigger as $no_change$
begin
if current_timestamp - old.date <= interval '5y' then
new = old;
end if;
return new;
end;
$no_change$ language plpgsql;
create trigger no_change after delete on wiz
for each row execute procedure no_change();
Not tested, but should work.
create or replace function no_change()
returns trigger as $no_change$
begin
if current_timestamp - old.date <= interval '5y' then
RETURN NULL;
ELSE
RETURN OLD;
end if;
end;
$no_change$ language plpgsql;
create trigger no_change BEFORE delete on wiz
for each row execute procedure no_change();
In the PostgreSQL docs:
Row-level triggers fired BEFORE can return null to signal the trigger manager to skip the rest of the operation for this row (i.e., subsequent triggers are not fired, and the INSERT/UPDATE/DELETE does not occur for this row).
<...>
In the case of a before-trigger on DELETE, the returned value has no direct effect, but it has to be nonnull to allow the trigger action to proceed. Note that NEW is null in DELETE triggers, so returning that is usually not sensible. The usual idiom in DELETE triggers is to return OLD.
So just return NULL in cases the date is less then 5 days old.

Postgresql Trigger Syntax Insert or Update or Delete

I am a newbie and I want to understand:
I understand that New is only executed with Insert or Update.
The OLD ones only with Delete "If I'm not mistaken".
And what I'm trying to do is derive "If 2 values ​​are the same but different numbers" which I insert into a table and leave the default false value, I did, but "I insert" the new data, but it should be the value "OLD" of "driver".
¿Could that request be fulfilled?
db<>fiddle
The trigger:
CREATE FUNCTION TR_DRIVER() RETURNS TRIGGER
AS
$$
BEGIN
IF EXISTS(SELECT number_driver, cod_driver
FROM driver AS con
WHERE EXISTS (SELECT * FROM driver_tmp AS tmp
WHERE con.number_driver<>tmp.number_driver AND con.cod_driver=tmp.cod_driver)) THEN
INSERT INTO driver_false(number_driver, cod_driver, full_name)
VALUES (new.number_driver, new.cod_driver, new.full_name);
ELSE
INSERT INTO driver(number_driver, cod_driver, full_name, active)
VALUES (new.number_driver, new.cod_driver, new.full_name, new.active)
ON CONFLICT (number_driver)
DO UPDATE SET
cod_driver=excluded.cod_driver,
full_name=excluded.full_name,
active=excluded.active;
END IF;
RETURN NEW;
END $$
LANGUAGE plpgsql;
CREATE TRIGGER TR_DRIVER_TMP AFTER INSERT OR UPDATE ON driver_tmp
FOR EACH ROW
EXECUTE PROCEDURE TR_DRIVER() ;
You are mistaken, OLD also exists for UPDATE as update in Postgres is actually INSERT/DELETE. For the details see:
https://www.postgresql.org/docs/current/plpgsql-trigger.html
NEW
Data type RECORD; variable holding the new database row for INSERT/UPDATE operations in row-level triggers. This variable is null in statement-level triggers and for DELETE operations.
OLD
Data type RECORD; variable holding the old database row for UPDATE/DELETE operations in row-level triggers. This variable is null in statement-level triggers and for INSERT operations.

Postgres 'after insert or update' trigger isn't firing

I've migrated our database from Oracle to Postgres. One of the steps is to save a copy of the record's status history. I do this via an INSERT, UPDATE trigger on the main table. The trigger checks to see if the status has changed and, if so, adds a record to the status history table.
CREATE OR REPLACE FUNCTION SAMPLE_STATUS_HISTORY_Trigger()
RETURNS TRIGGER AS $$
BEGIN
IF( old.STATUS_CODE != new.STATUS_CODE ) THEN
INSERT INTO SAMPLE_STATUS_HISTORY( analyte_status_history_id, sample_result_id, STATUS_CODE, status_date, status_user_id )
VALUES( nextval('SAMPLE_ANALYTE_STATUS_HIST_SEQ'), new.sample_result_id, new.STATUS_CODE, new.status_date, new.status_user_id );
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER TRG_SAMPLE_ANALYTE_STATUS_HIST
AFTER INSERT OR UPDATE ON SAMPLE_RESULTS
FOR EACH ROW
EXECUTE PROCEDURE SAMPLE_STATUS_HISTORY_Trigger()
;
I've ensured the trigger is bound to the table.
The main application is a .NET MVC app using NHibernate. Things work within a transaction, but the transactions are working for the main table's insert/updates.
My only guess is that I have a syntax error in the trigger logic but I'm not seeing the error if I manually run an insert or update statement.
For insert, OLD.? is null so the inequality check will also return null.
Instead, use
IF( old.STATUS_CODE IS DISTINCT FROM new.STATUS_CODE ) THEN
The INSERT with having a null value for old. was causing me issues when I tried #JGH's suggestion. I ended up with this code which just feels dirty:
IF( TG_OP = 'INSERT' OR old.STATUS_CODE <> new.STATUS_CODE ) THEN
I still don't understand why the original code wouldn't work for UPDATE statements. The only difference between this and that is the !=. Maybe that's a syntax issue for plpgsql?
Either way, this appears to work from both a SQL window and the application.

Postgres triggers and producers (column "new" of relation does not exist)

I am trying to create a trigger and procedure to update a last_changed_timestamp column upon UPDATE and INSERT.
I can register the function and trigger just fine, but when I try to update a record I receive the error.
CREATE OR REPLACE FUNCTION update_my_table_last_changed_timestamp()
RETURNS trigger AS
$BODY$
BEGIN
UPDATE my_table SET NEW.last_changed_timestamp = NOW();
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER trigger_update_my_table_last_changed_timestamp
BEFORE UPDATE
ON my_table
FOR EACH ROW
EXECUTE PROCEDURE update_my_table_last_changed_timestamp();
column "new" of relation "my_table" does not exist
I also do not fully understand how update_my_table_last_changed_timestamp knows which row it's suppose to update, nor if there were parameters passed to it, how the I would get those variables from the trigger to the procedure.
Modify the NEW record, there is no need to update.
BEGIN
NEW.last_changed_timestamp = NOW();
RETURN NEW;
END;
Read in the documentation: Overview of Trigger Behavior
If you still want to access a (other )table in the update trigger.
You can add to beginning of your trigger body the following:
EXECUTE format('SET search_path TO %I', TG_TABLE_SCHEMA);
For some reason with the update trigger it can happen that you're not on the correct search_path (i believe some old psql version have this)