Default values in Postgres view with triggered instead-of update - postgresql

I was planning on having a view with instead of insert trigger. There seems to be a problem with inserting a default value though.
Having the trigger set up as below, the following query fails
INSERT INTO v1.clients (foo) VALUES ('bar')
this returns a
null value in column "is_admin" violates not-null constraint
even though the underlying data.users (not null) table has a default value set.
What I'd say happens is the view translates all missing values to null and the instead of is applied, trying to insert null to a not null default false column.
Can I somehow set up the trigger to instead of trying to insert null to insert the default value? Having coalesce(NEW.is_admin, default) in the relevant insert in the trigger is a syntax error. I would rather not duplicate the default value manually in trigger.
Is this supported in postgres? What would be the best approach to split a view of two tables to those tables, while allowing default values?
definitions:
CREATE OR REPLACE VIEW v1.clients AS
SELECT
c.id, c.foo,
u.id user_id, u.is_admin
FROM data.clients c
INNER JOIN data.users u ON u.client_id = c.id;
CREATE FUNCTION data.separate_client_user_data()
RETURNS TRIGGER AS $$
DECLARE
client_id clients.id%TYPE;
BEGIN
INSERT INTO data.clients (foo) VALUES (NEW.foo) RETURNING id INTO client_id;
INSERT INTO data.users (client_id, is_admin)
VALUES (client_id, NEW.is_admin);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER user_data_trigger
INSTEAD OF INSERT ON v1.clients
FOR EACH ROW EXECUTE PROCEDURE data.separate_client_user_data();

Late to the game, I know, but I have extra insight I want to share.
Note that, as Patrick said, the NEW variable that the TRIGGER PROCEDURE uses contains all the fields - with NULLs added for any fields that were not originally part of your INSERT statement.
As you pointed out, the underlying table has a default value for the is_admin field, and unfortunately that default value doesn't get a chance to be seen, because the NEW variable already has NEW.is_admin = NULL.
However, I want to point out a different solution from Patrick's. It's true that you can hand-code the IF checks (essentially re-implementing the default value logic!) in your plpgsql function. But this may feel non-optimal from a DRY perspective. Here was the key for me when I had your problem:
TL/DR
The default values of the view itself are honored when building the NEW variable. Therefore if you do ALTER TABLE <view_name> ALTER is_admin SET DEFAULT false it should cause false to be passed in to NEW.is_admin, instead of NULL.
Hope that helps!

In a trigger function, the NEW and OLD implicit parameters always contain all the fields of the underlying table or view, with NULL assigned to fields for which no data is available. This is the correct behaviour, otherwise you could never assign a value to a field with no data.
In case you have a field with a NULL value and you want to get the DEFAULT value upon INSERT instead, you should test for it prior to doing the INSERT:
CREATE FUNCTION data.separate_client_user_data() RETURNS trigger AS $$
DECLARE
cid clients.id%TYPE; -- Don't use variable with same name as a column
BEGIN
INSERT INTO data.clients (foo) VALUES (NEW.foo) RETURNING id INTO cid;
IF NEW.is_admin IS NULL THEN
INSERT INTO data.users (client_id)
VALUES (cid); -- Use default value for is_admin
ELSE
INSERT INTO data.users (client_id, is_admin)
VALUES (cid, NEW.is_admin);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

Related

How to upgrade table inside a trigger function in POSTGRESQL?

I would like to create a trigger function inside my database which checks, if the newly "inserted" value (max_bid) is at least +1 greater than the largest max_bid value currently in the table.
If this is the case, the max_bid value inside the table should be updated, although not with the newly "inserted" value, but instead it should be increased by 1.
For instance, if max_bid is 10 and the newly "inserted" max_bid is 20, the max_bid value inside the table should be increased by +1 (in this case 11).
I tried to do it with a trigger, but unfortunatelly it doesn't work. Please help me to solve this problem.
Here is my code:
CREATE TABLE bidtable (
mail_buyer VARCHAR(80) NOT NULL,
auction_id INTEGER NOT NULL,
max_bid INTEGER,
PRIMARY KEY (mail_buyer),
);
CREATE OR REPLACE FUNCTION max_bid()
RETURNS TRIGGER LANGUAGE PLPGSQL AS $$
DECLARE
current_maxbid INTEGER;
BEGIN
SELECT MAX(max_bid) INTO current_maxbid
FROM bidtable WHERE NEW.auction_id = OLD.auction_id;
IF (NEW.max_bid < (current_maxbid + 1)) THEN
RAISE EXCEPTION 'error';
RETURN NULL;
END IF;
UPDATE bidtable SET max_bid = (current_maxbid + 1)
WHERE NEW.auction_id = OLD.auction_id
AND NEW.mail_buyer = OLD.mail_buyer;
RETURN NEW;
END;
$$;
CREATE OR REPLACE TRIGGER max_bid_trigger
BEFORE INSERT
ON bidtable
FOR EACH ROW
EXECUTE PROCEDURE max_bid();
Thank you very much for your help.
In a trigger function that is called for an INSERT operation the OLD implicit record variable is null, which is probably the cause of "unfortunately it doesn't work".
Trigger function
In a case like this there is a much easier solution. First of all, disregard the value for max_bid upon input because you require a specific value in all cases. Instead, you are going to set it to that specific value in the function. The trigger function can then be simplified to:
CREATE OR REPLACE FUNCTION set_max_bid() -- Function name different from column name
RETURNS TRIGGER LANGUAGE PLPGSQL AS $$
BEGIN
SELECT MAX(max_bid) + 1 INTO NEW.max_bid
FROM bidtable
WHERE auction_id = NEW.auction_id;
RETURN NEW;
END; $$;
That's all there is to it for the trigger function. Update the trigger to the new function name and it should work.
Concurrency
As several comments to your question pointed out, you run the risk of getting duplicates. This will currently not generate an error because you do not have an appropriate constraint on your table. Avoiding duplicates requires a table constraint like:
UNIQUE (auction_id, max_bid)
You cannot deal with any concurrency issue in the trigger function because the INSERT operation will take place after the trigger function completes with a RETURN NEW statement. What would be the most appropriate way to deal with this depends on your application. Your options are table locking to block any concurrent inserts, or looping in a function until the insert succeeds.
Avoid the concurrency issue altogether
If you can change the structure of the bidtable table, you can get rid of the whole concurrency issue by changing your business logic to not require the max_bid column. The max_bid column appears to indicate the order in which bids were placed for each auction_id. If that is the case then you could add a serial column to your table and use that to indicate order of bids being placed (for all auctions). That serial column could then also be the PRIMARY KEY to make your table more agile (no indexing on a large text column). The table would look something like this:
CREATE TABLE bidtable (
id SERIAL PRIMARY KEY,
mail_buyer VARCHAR(80) NOT NULL,
auction_id INTEGER NOT NULL
);
You can drop your trigger and trigger function and just depend on the proper id value being supplied by the system.
The bids for a specific action can then be extracted using a straightforward SELECT:
SELECT id, mail_buyer
FROM bidtable
WHERE auction_id = xxx
ORDER BY id;
If you require a max_bid-like value (the id values increment over the full set of auctions), you can use a simple window function:
SELECT mail_buyer, row_number() AS max_bid OVER (PARTITION BY auction_id ORDER BY id)
FROM bidtable
WHERE auction_id = xxx;

Error when creating a generated column in Postgresql

CREATE TABLE Person (
id serial primary key,
accNum text UNIQUE GENERATED ALWAYS AS (
concat(right(cast(extract year from current_date) as text), 2), cast(id as text)) STORED
);
Error: generation expression is not immutable
The goal is to populate the accNum field with YYid where YY is the last two letters of the year when the person was added.
I also tried the '||' operator but it was unsuccessful.
As you don't expect the column to be updated, when the row is changed, you can define your own function that generates the number:
create function generate_acc_num(id int)
returns text
as
$$
select to_char(current_date, 'YY')||id::text;
$$
language sql
immutable; --<< this is lying to Postgres!
Note that you should never use this function for any other purpose. Especially not as an index expression.
Then you can use that in a generated column:
CREATE TABLE Person
(
id integer generated always as identity primary key,
acc_num text UNIQUE GENERATED ALWAYS AS (generate_acc_num(id)) STORED
);
As #ScottNeville correctly mentioned:
CURRENT_DATE is not immutable. So it cannot be used int a GENERATED ALWAYS AS expression.
However, you can achieve this using a trigger nevertheless:
demo:db<>fiddle
CREATE FUNCTION accnum_trigger_function()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS $$
BEGIN
NEW.accNum := right(extract(year from current_date)::text, 2) || NEW.id::text;
RETURN NEW;
END
$$;
CREATE TRIGGER tr_accnum
BEFORE INSERT
ON person
FOR EACH ROW
EXECUTE PROCEDURE accnum_trigger_function();
As #a_horse_with_no_name mentioned correctly in the comments: You can simplify the expression to:
NEW.accNum := to_char(current_date, 'YY') || NEW.id;
I am not exactly sure how to solve this problem (maybe a trigger), but current_date is a stable function not an immutable one. For the generated IDs I believe all function calls must be immutable. You can read more here https://www.postgresql.org/docs/current/xfunc-volatility.html
I dont think any function that gets the date can be immutable as Postgres defines this as "An IMMUTABLE function cannot modify the database and is guaranteed to return the same results given the same arguments forever." This will not be true for anything that returns the current date.
I think your best bet would be to do this with a trigger so on insert it sets the value.

Eliminating the value of a field on a Postgres Trigger

I want to create a Postgres trigger that will eliminate a whole field from NEW like in (because this column will be filled automatically by the database and I do not want to change the assigned value by the database):
CREATE OR REPLACE FUNCTION befo_insert_for_auto_inc_numeric_ids()
RETURNS trigger AS
$$
BEGIN
NEW.Id = NULL; <---- Want to eliminate Id field from NEW
RETURN NEW;
END;
$$
LANGUAGE 'plpgsql';
Is this possible?
Assigning NULL to the column will achieve the opposite of what you want: if will manually set the column's value to NULL which will fail if the column is defined as NOT NULL
If you want to make sure that the ID is generated always regardless of what was provided in the in the actual INSERT statement, you can use nextval() inside the trigger:
CREATE OR REPLACE FUNCTION before_insert_for_auto_inc_numeric_ids()
RETURNS trigger AS
$$
BEGIN
NEW.id := nextval('id_column_seq');
RETURN NEW;
END;
$$
LANGUAGE plpgsql;
Replace id_column_seq with the name of the sequence attached to that column.
Note that this will most probably cause confusion at some point because values passed in the INSERT statement might not be the value stored in the table.

PLPGSQL function returning trigger AND value

I have written a PL/PGSQL function that returns a trigger, so I can call it before each row insert. I realize now that I would also like that function to return the ID of the newly inserted row. I'm not quite sure how to proceed since my function must return a trigger. Here's some code:
CREATE OR REPLACE FUNCTION f_insert_album() RETURNS TRIGGER AS $$
DECLARE
subj_album_id INTEGER;
BEGIN
-- ... some parts where left out
INSERT INTO t_albums_subjective (user_id, album_id, format_id, location_id, rating)
VALUES (NEW.user_id, obj_album_id, NEW.format_id, NEW.location_id, NEW.rating)
RETURNING id INTO subj_album_id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Bind insert function to trigger
DROP TRIGGER IF EXISTS tr_v_albums_insert ON v_albums;
CREATE TRIGGER tr_v_albums_insert INSTEAD OF INSERT ON v_albums
FOR EACH ROW EXECUTE PROCEDURE f_insert_album();
I must keep the return type of my function f_insert_album() to TRIGGER, but I would really like to also return the value in subj_album_id, corresponding to the id of the newly inserted row.
Is there anyway to do this? Is it even possible? Obviously changing the return type didn't work with Postgres. Could you suggest an alternative approach if any?
The crucial question: where to return the ID to?
Assuming you want to return it from the INSERT statement directly, then you are almost there. You already assign the newly generated ID to a function parameter:
...
RETURNING id INTO subj_album_id;
Instead, assign it to a column of the row firing the trigger. The special variable NEW holds this row in a trigger function:
...
RETURNING id INTO NEW.album_id; -- use actual column name in view
Then use the RETURNING clause of the INSERT statement:
INSERT INTO v_albums (user_id, format_id, location_id, rating)
VALUES ( ... )
RETURNING album_id;
Obviously, this is only possible if there is a visible column in the view. It does not have to be assigned in the INSERT command, though. The type of the NEW row variable is defined by the definition of the view, not by the INSERT at hand.
Closely related:
RETURNING data from updatable view not working?
For what you seem to be doing (grant access to certain rows of a table to a certain role) row level security (RLS) in Postgres 9.5 or later might be a more convenient alternative:
CREATE POLICY in the manual
The Postgres Wiki: "What's new in PostgreSQL 9.5"
Review by Depesz

Upon insert, how can I programmatically convert a null value to the column's default value?

I have a table in for which I have provided default values for some of its columns. I want to create a function with arguments corresponding to the columns that will insert a record in the table after modifying any null value to the column's default value.I dont want to programmatically construct the query based on which arguments are null. Essentially I would like something like
INSERT into Table (c1, c2, c3, c4)
Values (coalesce(somevar, DEFAULT(c1)), ...)
Is this possible? I ve seen that mysql can do this. Does postgres offer anything similar?
I am using version 9.1
UPDATE: This question provides some interesting solution but unfortunately the results are always text. I would like to get the default value as its true datatype so that I can use it for inserting it. I have tried to find a solution that will cast the default value from text to its datatype (which is provided as text) but I can't find a way:
SELECT column_name, column_default, data_type
FROM information_schema.columns
WHERE (table_schema, table_name) = ('public', 'mytable')
AND column_name = 'mycolumn'
ORDER BY ordinal_position;
The above returns the column_default and data_type as text so how can I cast the column_default to the value of data_type? If I could do this, then my problem would be solved.
If the table definition accepts an INSERT with the default values for all columns, the two-steps method below may work:
CREATE FUNCTION insert_func(c1 typename, c2 typename, ...)
RETURNS VOID AS $$
DECLARE
r record;
BEGIN
INSERT into the_table default values returning *,ctid INTO r;
UPDATE the_table set
col1=coalesce(c1,r.col1),
col2=coalesce(c2,r.col2),
...
WHERE the_table.ctid=r.ctid;
END;
$$ language plpgsql;
The trick is to get all the default values into the r record variable while inserting and use them subsequently in an UPDATE to replace any non-default value. ctid is a pseudo-column that designates the internal unique ID of the row that has just been inserted.
Caveat: this method won't work if some columns have a default null value AND a non-null check (or any check that implies that the default value is not accepted), since the first INSERT would fail.
I ve worked around my problem with a solution similar to Daniel's by creating a temp table with LIKE and INCLUDING DEFAULTS clauses in order to match my rowtype, then i use
INSERT INTO temp_table (c1, c2, ...) VALUES(x1, DEFAULT, ..)
using the default keyword for whatever column i am interested in. Then I insert to the real table by selecting from the temporary and using
VALUES( x1, coalesce(x2, temp_table.c2), ...).
I dont like it, but it works ok: I can select which on which columns I would like to do this "null-replace-with-default" check and it could work for many rows with one pass if I overload my function to accept a record array.