Auto updated column and generated always values - postgresql

Usually if I need to have some auto updated column as updated_at I used function and trigger. For example as it is described here.
In Postgres 12 we got generated columns. It does not give ability to use now() function directly, however I could create my own function wrapping it:
CREATE FUNCTION
now_time()
RETURNS timestamptz
AS $CODE$
BEGIN
RETURN now();
END
$CODE$
LANGUAGE plpgsql IMMUTABLE;
create table user(
id serial primary key,
name varchar,
updated_at timestamptz not null generated always as (now_time()) stored
);
And this works.
Which unwanted side effects could I get? Is such way better then old good trigger?

The one-side effect would be,
The value of the column would change if the database is restored from a pg_dump.

Related

First values in an auto increment trigger

I am working with the following table in PostgreSQL 10.3:
CREATE TABLE s_etpta.tab1 (
Number VARCHAR(40) NOT NULL,
id VARCHAR(8),
CONSTRAINT i_tab1 PRIMARY KEY(Number)
)
I need to increment the column id by 1 with every insert. I can't alter the table because I'm not the owner so I have no other choice than to increment a varchar column.
The column is type varchar prefixed with zeros. How can I specify that I want to start with '00000001' if the table is empty? Because when I already have values in my table the trigger gets the last value and increment it for the next insert which is correct, but when my table is empty the id column stays empty since the trigger has no value to increment.
CREATE OR REPLACE FUNCTION schema."Num" (
)
RETURNS trigger AS
$body$
DECLARE
BEGIN
NEW.id := lpad(CAST(CAST(max (id) AS INTEGER)+1 as varchar),8, '0') from
schema.tab1;
return NEW;
END;
$body$
LANGUAGE 'plpgsql'
VOLATILE
RETURNS NULL ON NULL INPUT
SECURITY INVOKER
COST 100;
A trigger design is unsafe and expensive trickery that can easily fail under concurrent write load. Don't use a trigger. Use a serial or IDENTITY column instead:
Auto increment table column
Don't use text (or varchar) for a numeric value.
Don't pad leading zeroes. You can format your numbers any way you like for display with to_char():
How to auto increment id with a character
In Postgres 10 or later your table could look like this:
CREATE TABLE s_etpta.tab1 (
number numeric NOT NULL PRIMARY KEY, -- not VARCHAR(40)
id bigint GENERATED ALWAYS AS IDENTITY -- or just int?
);
No trigger.
Seems odd that number is the PK. Would seem like id should be. Maybe you do not need the id column in the table at all?
Gap-less sequence where multiple transactions with multiple tables are involved
If you need to get the underlying sequence in sync:
How to reset postgres' primary key sequence when it falls out of sync?
Postgres manually alter sequence
If you cannot fix your table, this trigger function works with the existing one (unreliably under concurrent write load):
CREATE OR REPLACE FUNCTION schema.tab1_number_inc()
RETURNS trigger AS
$func$
DECLARE
BEGIN
SELECT to_char(COALESCE(max(id)::int + 1, 0), 'FM00000000')
FROM schema.tab1
INTO NEW.id;
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
Trigger:
CREATE TRIGGER tab1_before_insert
BEFORE INSERT ON schema.tab1
FOR EACH ROW EXECUTE PROCEDURE schema.tab1_number_inc();
The FM modifier removes leading blanks from to_char() output:
Remove blank-padding from to_char() output

Postgres trigger that will in any case set a value on modification?

I have a Web Application that has a modified field in the important tables to be able to track back when any modification was done e.g. (never mind the ;; it is there because this postgres sql code is executed from a Scala framework that uses ; as separator and ;; escapes it)
CREATE TABLE security_permission (
id BIGSERIAL,
value VARCHAR(255) NOT NULL,
modified TIMESTAMP DEFAULT now(),
PRIMARY KEY (id)
);
CREATE OR REPLACE FUNCTION update_modified()
RETURNS TRIGGER AS $$
BEGIN
NEW.modified = now();;
RETURN NEW;;
END;;
$$ language 'plpgsql';
CREATE TRIGGER update_modified_security_permission BEFORE UPDATE ON security_permission FOR EACH ROW EXECUTE PROCEDURE update_modified();
The problem is this works only if the field is NOT specified in the insert/update statement. If the field is specified even with NULL then the modified is not set. I do not have full control of the generated statements because they are part of an ORM framework that generates them automatically but I'd like to nevertheless always set the modified field. How can I do that?
I have tried using BEFORE INSERT OR UPDATE ON and AFTER INSERT OR UPDATE ON but nothing seems to work if the field is populated in the insert/update statement even if NULL. How can I do this?
Define the trigger as before update or insert:
CREATE TRIGGER update_modified_security_permission
BEFORE UPDATE OR INSERT ON security_permission
FOR EACH ROW EXECUTE PROCEDURE update_modified();
See a working example here.

Is it possible to pass data to postgreSQL trigger?

I need to log any changes made in some table by trigger which will insert older version of modified row to another table with some additional data like:
-which action was performed
-when this action was performed
-by who.
I have problem with last requirement. While performing SQL somewhere in java by JDBC. I need to somehow pass logged user id stored in variable to postgres table where all older versions of modified row will be stored.
Is it even possible?
It may be stupid question but I desperately try to avoid inserting data like that manually in java. Triggers done some work for me but not all I need.
Demonstrative code below (I've cut out some code for security reasons):
"notes" table:
CREATE TABLE my_database.notes
(
pk serial NOT NULL,
client_pk integer,
description text,
CONSTRAINT notes_pkey PRIMARY KEY (pk)
)
Table storing older versions of every row changed in "notes" table:
CREATE TABLE my_database_log.notes_log
(
pk serial NOT NULL,
note_pk integer,
client_pk integer,
description text,
who_changed integer DEFAULT 0, -- how to fill in this field?
action_date timestamp without time zone DEFAULT now(), --when action was performed
action character varying, --which action was performed
CONSTRAINT notes_log_pkey PRIMARY KEY (pk)
)
Trigger for "notes" table:
CREATE TRIGGER after_insert_or_update_note_trigger
AFTER INSERT OR UPDATE
ON database.notes
FOR EACH ROW
EXECUTE PROCEDURE my_database.notes_new_row_log();
Procedure executed by trigger:
CREATE OR REPLACE FUNCTION my_database.notes_new_row_log()
RETURNS trigger AS
$BODY$
BEGIN
INSERT INTO my_database_log.notes_log(
note_pk, client_pk, description, action)
VALUES (
NEW.pk, NEW.client_pk, NEW.description, TG_OP);
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
ALTER FUNCTION my_database.notes_new_row_log()
OWNER TO database_owner;
According to #Nick Barnes hint in comments, there is a need to declare a variable in postgresql.conf file:
...
#----------------------------------------------------------------------------
# CUSTOMIZED OPTIONS
#----------------------------------------------------------------------------
custom_variable_classes = 'myapp' # list of custom variable class names
myapp.user_id = 0
and call:
SET LOCAL customvar.user_id=<set_user_id_value_here>
before query that should be triggered.
To handle variable in trigger use:
current_setting('myapp.userid')

PostgreSQL trigger to make default value refer to another table

PostgreSQL default values cannot contain variables or refer to any other columns in the table, or in a different table.
However, is it possible to use a trigger to create a "Default value" that will behave in the following manner. First, let me illustrate with two example tables:
create table projects
(
id serial primary key,
created_at timestamp with time zone default now()
);
create table last_updated
(
project_id integer primary key references projects,
updated_at timestamp with time zone default ...
);
In the second table (last_updated) I would like the default to be something like default projects(created_at). I.e. if a date is not specified for updated_at, look at the project_id referenced in the projects table, find the created_at date, and set the updated_at to this date. However, you cannot write this as per the first paragraph of my question.
So how do you write a trigger that will give this functionality?
The correct answer depends on what you do not specify. Typically, one would make updates to the projects table and then audit that in the last_updated table, using an AFTER UPDATE trigger on table projects:
CREATE FUNCTION audit_project_update () RETURNS trigger AS $$
BEGIN
INSERT INTO last_updated VALUES
(NEW.id, -- NEW refers to the updated record in the projects table
now() -- this would be the logical value, but can use NEW.created_at
-- other columns, possibly log session_user
);
RETURN NEW;
END; $$ LANGUAGE plpgsql;
CREATE TRIGGER tr_projects_update
AFTER UPDATE ON projects
FOR EACH ROW EXECUTE PROCEDURE audit_project_update();
Note that in this approach there is never a situation where an INSERT is made on table last_updated without specifying a value for updated_at, assuming that you will not GRANT INSERT to any role on table last_updated, because the trigger function always specifies now(). In the table definition you do not have to specify a default value anymore: the trigger gives you the automated behavior you are looking for.
Your stated question - and confirmed in the comment below - would also use a trigger, but then on the last_updated table:
CREATE FUNCTION project_last_updated () RETURNS trigger AS $$
BEGIN
IF (NEW.updated_at IS NULL) THEN
SELECT created_at INTO NEW.updated_at
FROM projects
WHERE id = NEW.project_id;
END IF;
RETURN NEW;
END; $$ LANGUAGE plpgsql;
CREATE TRIGGER tr_projects_update
BEFORE INSERT ON last_updated
FOR EACH ROW EXECUTE PROCEDURE project_last_updated();
This specification begs the question why you do not simply add a column updated_at to the projects table. Since the project_id column is PK in the last_update table, you can only store a single last update date per project.

Manipulate rows automatically before the `INSERT` statement

I'm looking for a way to manipulate rows automatically before adding them to a table in postgreSQL. Say for instance we have the following table:
CREATE TABLE foo (
id serial NOT NULL,
value integer NOT NULL,
CONSTRAINT "Foo_pkey" PRIMARY KEY (id),
CONSTRAINT "Foo_value_check" CHECK (value >= 0)
)
Now one can insert rows:
INSERT INTO foo (id,value) VALUES ('0','2')
And when one enters:
INSERT INTO foo (id,value) VALUES ('1','-2')
An error will occur. Is it possible to define a "rewrite rule" that given the value column contains a value less than zero, zero is used (for instance)?
Yes, it is possible. One way is to use triggers. A trigger causes a procedure to be run on particular actions, which can allow you to modify the data to be inserted (amongst other things).
To set up a trigger, you first create a function that will perform the checks and modifications you want. The variable new in your function will be implicitly declared and contain the new row to be inserted / updated so you can check and modify the values before they reach the table.
You then specify that this function is to be called before insert or update on one or more tables.
Example:
CREATE FUNCTION validate_foo_row()
RETURNS TRIGGER AS $$
BEGIN
IF new.value<0 THEN
new.value=0;
END IF;
RETURN NEW;
END
$$ LANGUAGE 'plpgsql';
CREATE TRIGGER trig_validate_foo BEFORE INSERT ON foo
FOR EACH ROW EXECUTE PROCEDURE validate_foo_row();
SqlFiddle Here
The above simplistic example only triggers for inserts, you might want to have it trigger for updates as well.
You can read more about triggers in the postgresql manual. They are powerful and are capable of a lot more than this simple example shows.