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();
Related
I'm trying to implement table partition and I have the below code in PostgreSQL (source from https://www.postgresql.org/docs/9.6/ddl-partitioning.html )
CREATE or replace FUNCTION child_tables.func_inventory_movement_insert_trigger()
RETURNS trigger
LANGUAGE 'plpgsql'
COST 100
VOLATILE NOT LEAKPROOF
AS $BODY$BEGIN
IF ( NEW.report_date >= '2019-04-01 ' AND
NEW.report_date < '2019-05-01 ' ) THEN
INSERT INTO child_tables.inventory_movement_y2019m03 VALUES (NEW.*);
ELSIF ( NEW.report_date >= '2019-06-01 ' AND
NEW.report_date < '2019-07-01 ' ) THEN
INSERT INTO child_tables.inventory_movement_y2019m04 VALUES (NEW.*);
ELSE
RAISE EXCEPTION
' out of range exception. Fix the child_tables.func_inventory_movement_insert_trigger() function. ';
END IF;
RETURN NULL;
END;
$BODY$;
Trigger function:
CREATE TRIGGER im_partition_trigger
BEFORE INSERT OR DELETE OR UPDATE
ON core.inventory_movement
FOR EACH ROW
EXECUTE PROCEDURE child_tables.func_inventory_movement_insert_trigger();
Tried for both after or before in above-mentioned trigger.
ERROR: record "new" is not assigned yet
DETAIL: The tuple structure of a not-yet-assigned record is indeterminate.
FYI, same code is working for the other table.
Any suggestions?
Your trigger is defined
BEFORE INSERT OR DELETE OR UPDATE
But NEW is not defined for a DELETE . So your trigger function is bound to fail like it did.
Either write separate trigger functions and triggers for INSERT / UPDATE / DELETE (recommended). The displayed trigger function only deals with INSERT. So this trigger would make sense:
CREATE TRIGGER im_partition_trigger
BEFORE INSERT -- !!
ON core.inventory_movement
FOR EACH ROW EXECUTE PROCEDURE child_tables.func_inventory_movement_insert_trigger();
Or nest all calls to OLD and NEW in acombined trigger function in IF or CASE constructs, checking for TG_OP.
Code examples in many related questions.
That said, to implement table partitioning I would much rather use declarative partitioning in Postgres 10 or later. Ideally, use the upcoming Postgres 12, which brings big improvements for partitioning.
Aside, in Postgres 11 or later, rather use the fixed syntax for triggers:
...
FOR EACH ROW EXECUTE FUNCTION child_tables.func_inventory_movement_insert_trigger();
It's a function, not a "procedure".
Postgresql 10/11.
I need to delete row instead of update in case if target cell value is null.
So I created this trigger function:
CREATE OR REPLACE FUNCTION delete_on_update_related_table() RETURNS trigger
AS $$
DECLARE
refColumnName text = TG_ARGV[0];
BEGIN
IF TG_NARGS <> 1 THEN
RAISE EXCEPTION 'Trigger function expects 1 parameters, but got %', TG_NARGS;
END IF;
EXECUTE 'DELETE FROM ' || TG_TABLE_NAME || ' WHERE $1 = ''$2'''
USING refColumnName, OLD.id;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
And a BEFORE UPDATE trigger:
CREATE TRIGGER proper_delete
BEFORE UPDATE OF def_id
ON public.definition_products
FOR EACH ROW
WHEN (NEW.def_id IS NULL)
EXECUTE PROCEDURE delete_on_update_related_table('def_id');
Table is simple:
id uuid primary key
def_id uuid not null
Test:
UPDATE definition_products SET
def_id = NULL
WHERE id = 'f47415e8-6b00-4c65-aeb8-cadc15ca5890';
-- rows affected 0
Documentation says:
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).
Previously, I used a RULE instead of the trigger. But there is no way to use WHERE & RETURNING clause in same rule.
You need an unconditional ON UPDATE DO INSTEAD rule with a RETURNING clause
So, is there a way?
While Jeremy's answer is good, there is still room for improvement.
Problems
You need to be very accurate in the definition of the objective. Your statement:
I need to delete row instead of update in case if target cell value is null.
... does not imply that the column was changed to NULL in the UPDATE at hand. Might have been NULL before, like, before you implemented the trigger. So not:
BEFORE UPDATE OF def_id ON public.definition_products
But just:
BEFORE UPDATE ON public.definition_products
Of course, if the column is defined NOT NULL (as it probably should be), there is no effective difference - except for the noise and an additional point of failure. The manual:
A column-specific trigger (one defined using the UPDATE OFcolumn_name syntax) will fire when any of its columns are listed as targets in the UPDATE command's SET list. It is possible for a column's value to change even when the trigger is not fired, because changes made to the row's contents by BEFORE UPDATE triggers are not considered.
Also, nothing in your question indicates the need for dynamic SQL. (That would be the case if you wanted to reuse the same trigger function for multiple triggers on different tables. And even then it's often better to just create several distinct trigger functions for multiple reason: simpler, faster, less error-prone, easier to read & maintain, ...)
As for "error-prone": your original dynamic statement was just invalid:
EXECUTE 'DELETE FROM ' || TG_TABLE_NAME || ' WHERE $1 = ''$2'''
USING refColumnName, OLD.id;
Can't pass a column name as value (refColumnName).
Can't put single quotes around $2, which is passed as value and hence needs no quoting.
An unqualified, unquoted TG_TABLE_NAME can go terribly wrong, which is especially critical for a heavy-weight function that deletes rows.
Jeremy's version fixes most, but still features the unqualified TG_TABLE_NAME.
This would be good:
EXECUTE format('DELETE FROM %s WHERE %I = $1', TG_RELID::regclass, refColumnName) -- refColumnName still unquoted
USING OLD.id;
Or:
EXECUTE format('DELETE FROM %I.%I WHERE %I = $1', TG_TABLE_SCHEMA, TG_TABLE_NAME, refColumnName)
USING OLD.id;
Related:
Why does a PostgreSQL SELECT query return different results when a schema name is specified?
Table name as a PostgreSQL function parameter
Solution
Simpler trigger function:
CREATE OR REPLACE FUNCTION delete_on_update_related_table()
RETURNS trigger AS
$func$
BEGIN
DELETE FROM public.definition_products WHERE id = OLD.id; -- def_id?
RETURN NULL;
END
$func$ LANGUAGE plpgsql;
Simpler trigger:
CREATE TRIGGER proper_delete
BEFORE UPDATE ON public.definition_products
FOR EACH ROW
WHEN (NEW.def_id IS NULL) -- that's the defining condition!
EXECUTE PROCEDURE delete_on_update_related_table(); -- no parameter
You probably want to use OLD.id, not OLD.def_id. (The row to delete is best defined by it's PK, not by the column changed to NULL.) But that's not entirely clear.
This works for me, with a few small changes:
CREATE OR REPLACE FUNCTION delete_on_update_related_table() RETURNS trigger
AS $$
DECLARE
refColumnName text = quote_ident(TG_ARGV[0]);
BEGIN
IF TG_NARGS <> 1 THEN RAISE EXCEPTION 'Trigger function expects 1 parameters, but got %', TG_NARGS; END IF;
EXECUTE format('DELETE FROM %s WHERE %s = %s', quote_ident(TG_TABLE_NAME), refColumnName, quote_literal(OLD.id));
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
-- create trigger
CREATE TRIGGER proper_delete
BEFORE UPDATE OF def_id
ON public.definition_products
FOR EACH ROW
WHEN (NEW.def_id IS NULL)
EXECUTE PROCEDURE delete_on_update_related_table('id'); --Note id, not def_id
I have a table with companies, and one of the columns is their status, which can be active as A or deactivated as D, and i am trying to make a trigger to check if the company is active so they can pay taxes or deactivated and they wont be bothered.
CREATE TRIGGER StatusCheck
BEFORE INSERT
ON Companies C
REFERENCING NEW AS N --row to be added
FOR EACH ROW
WHEN c.status <> 'A' or c.status <>'D';
ABORT TRANSACTION
This clearly doesnt work, but how can i do this, because i have been overthinking about this and now i think i need some help.
The correct and most efficient way to implement such a requirement is a check constraint
alter table companies
add constraint check_status check (status in ('A', 'D'));
If you are forced to use a trigger:
You create trigger statement has two errors:
The first one is a syntax error: there is no abort transaction option in the [create trigger][1] statement.
The second one is a logical error: the condition c.status <> 'A' or c.status <>'D' is essentially always true. What you want there is an AND condition: "if the status value is different than A and different than D...".
However a complete trigger definition in Postgres consists of a trigger function and the create trigger statement attaching that function to a table trigger.
Inside the function you would need to throw an exception to abort the transaction:
create function check_company_status()
returns trigger
as
$$
begin
if not new.status in ('A', 'D') then
raise 'Invalid valid for status specified';
end if;
return new;
end;
$$
language plpgsql;
Now that we have a trigger function, we can attach that to a trigger on the table:
create trigger check_status_trigger
before insert or update
on companies
for each row
execute procedure check_company_status();
It's important to define that as a BEFORE trigger.
Note that neither of those solutions will prevent NULL values in that column. If you also want to prevent NULL, the column should be defined as NOT NULL
I am trying to set up triggers for insert and update events for the master table of some partition tables in PostgreSQL. Each time an insertion is made into the master table, the insert trigger event will redirect it into the correct partition table. Consequently, I will need to return NULL from this function call, since I don't want the master table to be populated as well. If the master table receives an update event, it will update a timestamp before making the change in the table. The problem is that the update trigger is never fired. I am using PostgreSQL version 9.6.
I have tried to combine the trigger functions into one, and merged the called trigger procedures into one as well, but the results are the same. The update trigger is only triggered if I return NEW from the insertion trigger function (which populates the master table), or if I comment out the insertion trigger function altogether.
DROP SCHEMA IF EXISTS test CASCADE;
CREATE SCHEMA test;
SET SCHEMA 'test';
CREATE TYPE test_type AS ENUM ('unit', 'performance');
CREATE TABLE test (
type test_type NOT NULL,
score INTEGER NOT NULL CHECK (score > 0),
id SERIAL PRIMARY KEY,
updated_at TIMESTAMP DEFAULT current_timestamp
);
CREATE TABLE performance_test (
CHECK (type = 'performance')
) INHERITS (test);
CREATE FUNCTION insert_test()
RETURNS trigger AS
$$
BEGIN
INSERT INTO performance_test VALUES (NEW.*);
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE FUNCTION update_timestamp()
RETURNS trigger AS
$$
BEGIN
RAISE NOTICE 'This is never reached.';
UPDATE performance_test
SET updated_at = current_timestamp
WHERE id = NEW.id;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER test_insertion BEFORE INSERT ON test
FOR EACH ROW EXECUTE PROCEDURE insert_test();
CREATE TRIGGER test_update BEFORE UPDATE ON test
FOR EACH ROW EXECUTE PROCEDURE update_timestamp();
---------------------------------------------------------------------------
INSERT INTO test VALUES ('performance', 10);
SELECT * FROM performance_test;
UPDATE test SET score = 20 WHERE id = 1;
SELECT * FROM performance_test;
I am not sure if it is possible to achieve what I want with this method, so I'm reaching out here for any advice. Thanks in advance!
/ Hampus
Row triggers must be defined on individual partitions, not the partitioned table. See https://www.postgresql.org/docs/10/ddl-partitioning.html#DDL-PARTITIONING-DECLARATIVE-LIMITATIONS
I don't know why the documentation for 9.6 doesn't mention this
working update trigger:
CREATE FUNCTION update_timestamp()
RETURNS trigger AS
$$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER test_update BEFORE UPDATE ON performance_test
FOR EACH ROW EXECUTE PROCEDURE update_timestamp();
if you do UPDATE test SET score = 30, updated_at=DEFAULT; or UPDATE test SET score = 30, updated_at=current_timestamp; you might not need the update trigger.
Partitioning is not a free lunch because it has non-obvious effects on both behavior and performance, as you noticed by the trigger not behaving as you expected. If you make a mistake it can easily lead to failing queries and even bad data.
If you are really sure you need it you should make sure you understand it in detail and otherwise I'd recommend you to avoid it, most issues with slow queries can be solved by making sure the table statistics is up to date, using the right indexes, optimizing queries, changing Postgres configuration or adding more hardware.
I've created the following table:
CREATE TABLE updates
(
"table" text,
last_update timestamp without time zone
)
I want to update it whenever any table is updated, the problem is I don't know how, could someone please help me turn this pseudocode into a trigger?
this = current table on whitch operation is performed
ON ALTER,INSERT,DELETE {
IF (SELECT COUNT(*) FROM updates where table = this) = 1
THEN
UPDATE updates SET last_update = timeofday()::timestamp WHERE `table`=this
ELSE
INSERT INTO updates VALUES (this,timeofday()::timestamp);
}
You need a trigger function that is called whenever one of your tables is "updated", assuming that you mean that an INSERT, UPDATE, or DELETE is successfully executed. That trigger function would look like this:
CREATE FUNCTION log_update() RETURNS trigger AS $$
BEGIN
UPDATE updates SET last_update = now() WHERE "table" = TG_TABLE_NAME;
IF NOT FOUND THEN
INSERT INTO updates VALUES (TG_TABLE_NAME, now());
END IF;
IF (TG_OP = 'DELETE') THEN
RETURN OLD;
ELSE
RETURN NEW;
END IF;
END; $$ LANGUAGE PLPGSQL;
Every table that has to be logged this way needs to have a trigger associated with it like this:
CREATE TRIGGER ZZZ_mytable_log_updates
AFTER INSERT OR UPDATE OR DELETE ON mytable
FOR EACH ROW EXECUTE PROCEDURE log_update();
A few comments:
Trigger functions are created with PL/PgSQL; see chapter 40 in the documentation. Trigger functions come with some automatic parameters such as TG_TABLE_NAME.
Don't use reserved words ("table" in your case) as column names. Actually, in this case you are better off using the oid of the table, with the associated TG_RELID automatic parameter. It takes up less storage, it is faster, and it avoids confusion between tables with the same name in different schemas of your database. You can use the pg_tables system catalog table to look up the table name from the oid.
You must return the proper value depending on the operation, or the operation may fail. INSERT and UPDATE operations need to have NEW returned; DELETE needs to have OLD returned.
The name of the trigger starts with "ZZZ" to make sure that it fires after any other triggers on the same table have succeeded (they are fired in alphabetical order). If a prior trigger fails, this trigger function will not be called, which is the proper behaviour because the insert, update or delete will not take place either.