Postgres and math on columns during insert or update - sql

When inserting or updating data I would like to be able to perform some math on two columns and have that entered as a value for a third column.
Table schema:
CREATE TABLE "public"."subscriptions" (
"id" int4 NOT NULL DEFAULT nextval('subscriptions_id_seq'::regclass),
"item" varchar,
"amount" float4,
"yearly_recurrance" int2,
"annual_cost" float4,
"rank" int2,
PRIMARY KEY ("id")
);
Insert Statement:
INSERT INTO "public"."subscriptions" ("item", "amount", "yearly_recurrance", "rank") VALUES ('test', '19', '7', '0');
I have created a function and trigger that in my mind should take the amount and multiply it by the yearly_recurrance and enter the result in the annual_cost field.
Function:
CREATE OR REPLACE FUNCTION public.calc_cost()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
BEGIN
NEW.annual_cost = NEW.amount * NEW.yearly_recurrance;
RETURN NEW;
END;
$function$
Trigger:
CREATE TRIGGER calc_cost BEFORE INSERT or UPDATE ON subscriptions
FOR EACH STATEMENT
EXECUTE PROCEDURE calc_cost();
It is not working out how I expect. Instead the values from the insert statement are put where they belong, but it is as though the function doesn't run. Not getting any errors that I can see.
I should mention that I believe the trigger is working and calling the function. If I put garbage in the function I get errors. I think the problem is in the function. Given this is my first function however, I am not sure how to work with uncommitted data. Perhaps it's not possible.

Use a generated column instead:
alter table subscriptions
add annual_cost float4 generated always as (amount * yearly_recurrance) stored;
No trigger overhead and it is always accurate.
Note: I don't recommend floats for monetary amounts; rounding errors can be problematic. Use numeric.

Related

how to make sure that trigger generated value is being returned?

I have this INSERT query, which purpose is to insert the one row in my database.
Similarly I also have a INSERT query which insert multiple rows.
One of the columns in the table is generated after the values has been generated, since it combines a set of column values to construct a name. The name itself it generated from a Trigger, and its triggered After insert, since the column values has to exist for me to generate the name.
my problem now is when I insert one row or multiple rows, I want to know the the generated column value, but when I return it, it states its null?
#$"INSERT INTO registration_table (id, ...,)
VALUES (1,...,)
RETURNING row_id, name;";
which in return gives me an id the one I inserted, but the not actual name but instead I get null..
The trigger is pretty straight forward
CREATE TRIGGER name_insert_trigger
AFTER INSERT
ON registration_table
REFERENCING NEW TABLE AS new_inserts
FOR EACH STATEMENT
WHEN (pg_trigger_depth() = 0)
EXECUTE PROCEDURE registration_entry_name();
CREATE OR REPLACE FUNCTION registration_entry_name()
RETURNS trigger AS
$$
DECLARE
BEGIN
UPDATE registration_table
SET name = |Pattern| -- This one being the actual name generated..
FROM new_inserts
WHERE new_inserts.row_id = registration_table.row_id;
RETURN null;
END;
$$
LANGUAGE plpgsql;
but the insert query above does not return the name?
why not?
You actually need a BEFORE trigger, your data values will be there. The designation of Before and After very often causes misconceptions especially of row level triggers. The terms do not indicate their timing in relation to the DML. I have found it useful to think of them as "before final data values are set" and "after final data values are set" but both run before the invoking DML completes (for now we will bypass deferred triggers). Lets look at inserts. When the before row trigger fires the NEW row contains the values at that point for every column in the row, any value not specified in the statement will be null or contain the specified default if any. Before row triggers can can change any column. After row triggers cannot change columns, if present any change is ignored.
Your description and code imply you need to combine a couple columns to generate the content of another. Since you did not specify exactly that I will build an example and demo.
create table users ( usr_id integer generated always as identity
, lname text not null
, fname text not null
, full_name text not null
) ;
create or replace
function users_bir()
returns trigger
language plpgsql
as $$
begin
if new.full_name is null
then
new.full_name = trim(new.fname) || ' ' || trim(new.lname);
end if;
return new;
end;
$$;
create trigger users_bir_trg
before insert on users
for each row
execute procedure users_bir();
insert into users(fname, lname)
values ( 'George', 'Henery')
, ( 'Samatha', 'van Horm');
insert into users(fname, lname, full_name)
values ( 'Wacky', 'Warriors','Not so tough guys');
This setup allows the full_name to be specified or generated. If only generation is desired remove the IF leaving only the assignment statement. Even better if you have Postgres 12 or higher just define the the column as a generated column. This is also in the demo.

How can I create a Postgres 11 trigger function which inserts a new row in table 'b' upon insert or update to table 'a'?

I'm running Postgres 11 on RDS.
I'm trying to create a simple trigger function to insert records into table 'test_alias' whenever a row is inserted into table 'test_values'.
I have the following tables:
CREATE TABLE the_schema.test_values (
id INTEGER NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
value_1 TEXT NOT NULL,
value_2 TEXT NOT NULL,
value_quantity INTEGER NOT NULL
);
CREATE TABLE the_schema.test_alias (
id INTEGER NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
value_1_copy TEXT NOT NULL,
value_2_copy TEXT NOT NULL,
value_quantity_copy INTEGER NOT NULL
);
My trigger function is like so:
CREATE OR REPLACE FUNCTION the_schema.populate_test_alias()
RETURNS TRIGGER AS
$BODY$
BEGIN
IF NEW.the_schema.test_values THEN
INSERT INTO the_schema.test_alias (value_1_copy, value_2_copy, value_quantity_copy)
VALUES (NEW.value_1, NEW.value_2, NEW.value_quantity);
END IF;
return null;
END;
$BODY$ LANGUAGE plpgsql;
And here is the trigger:
CREATE TRIGGER TRG_TEST_ALIAS
AFTER INSERT OR UPDATE ON the_schema.test_values
FOR EACH ROW
execute procedure the_schema.populate_test_alias();
Upon INSERT like so:
INSERT INTO the_schema.test_values (value_1, value_2, value_quantity)
VALUES ('abc', 'xyz', 1);
I get this error:
ERROR: missing FROM-clause entry for table "the_schema"
LINE 1: SELECT NEW.the_schema.test_values
I've also tried an equivalent setup with the default schema, and it still errors (though with a different error):
ERROR: record "new" has no field "test_values"
CONTEXT: SQL statement "SELECT NEW.test_values"
PL/pgSQL function populate_test_alias() line 3 at IF
It seems to me that there must be an error in the way I'm using the NEW keyword, but as far as I can tell, the way I've used it in the function is the same as several examples I've referred to (online/SO and hard copy), and I can't figure it out.
All guidance is much appreciated!
example of similar question for reference, includes links to official docs (which I've also read but clearly don't understand as I should):
[https://stackoverflow.com/questions/11001118/postgres-trigger-after-insert-accessing-new]
NEW references the inserted or updated row. Therefore NEW. only makes sense with a field identifier.
Also value_1, value_2 and value_quantity have a NOT NULL constraint, which means that you need not test for them.
So you can just drop the whole conditional:
CREATE OR REPLACE FUNCTION the_schema.populate_test_alias()
RETURNS TRIGGER AS
$BODY$
BEGIN
--IF NEW.the_schema.test_values THEN
INSERT INTO the_schema.test_alias (value_1_copy, value_2_copy, value_quantity_copy)
VALUES (NEW.value_1, NEW.value_2, NEW.value_quantity);
--END IF;
return null;
END;
$BODY$ LANGUAGE plpgsql;

Generic Postgres 9.5 trigger to convert an UPDATE into modified INSERT

Is it possible to create a generic (not table-specific) trigger in Postgres 9.5 that would perform on instead of update that converts the update into an insert?
Basically what I want to do is (pseudocode):
sql
instead of UPDATE on TG_TABLE_NAME INSERT on TG_TABLE_NAME
I know I can create a very table-specific trigger that maps each value into an insert statement. What I'm trying to do is get away from creating this trigger on every single table.
It is a bit of an oddball idea (nothing personal), but how about this:
CREATE FUNCTION not_update_but_insert() RETURNS trigger AS $$
BEGIN
INSERT INTO TG_TABLE_NAME -- Do an INSERT...
SELECT NEW.*; -- ... using the values from the row to be updated
RETURN NULL; -- Fail the UPDATE
END;
$$ LANGUAGE plpgsql;
Obviously this would not work for any table that has a PRIMARY KEY or other UNIQUE constraints. You do have to CREATE TRIGGER x BEFORE UPDATE for every table this would apply to, so analyze your table structure before creating the trigger.
There is obviously a work-around - at least for the PKs based on a sequence - by examining the information_schema for "safe" columns in TG_TABLE_NAME and then assembling their names into strings to splice into the INSERT statement (column list of main statement and select list). Just leave the columns with sequences or appropriate default values out. This, however, does not address UNIQUE constraints that have no obvious replacement (like a user name or an email address).

Trigger to update a different table

Using Postgres 9.4, I have 2 tables streams and comment_replies. I am trying to do is update the streams.comments count each time a new comment_replies is inserted to keep track of the number of comments a particular stream has. I am not getting any errors but when I try to create a new comment it gets ignored.
This is how I am setting up my trigger. stream_id is a foreign key, so every stream_id will correspond to a streams.id which is the primary key of the streams table. I have been looking at this example: Postgres trigger function, but haven't been able to get it to work.
CREATE TABLE comment_replies (
id serial NOT NULL PRIMARY KEY,
created_on timestamp without time zone,
comments text,
profile_id integer,
stream_id integer
);
The trigger function:
CREATE OR REPLACE FUNCTION "Comment_Updates"()
RETURNS trigger AS
$BODY$BEGIN
update streams set streams.comments=streams.comments+1
where streams.id=comment_replies_streamid;
END$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
And the trigger:
CREATE TRIGGER comment_add
BEFORE INSERT OR UPDATE
ON comment_replies
FOR EACH ROW
EXECUTE PROCEDURE "Comment_Updates"();
How can I do this?
There are multiple errors. Try instead:
CREATE OR REPLACE FUNCTION comment_update()
RETURNS trigger AS
$func$
BEGIN
UPDATE streams s
SET streams.comments = s.comments + 1
-- SET comments = COALESCE(s.comments, 0) + 1 -- if the column can be NULL
WHERE s.id = NEW.streamid;
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
CREATE TRIGGER comment_add
BEFORE INSERT OR UPDATE ON comment_replies -- on UPDATE, too? Really?
FOR EACH ROW EXECUTE PROCEDURE comment_update();
You need to consider DELETE as well if that is possible. Also if UPDATE can change stream_id. But why increase the count for every UPDATE? This looks like another error to me.
It's a syntax error to table-qualify the target column in the SET clause of UPDATE.
You need to return NEW in a BEFORE trigger unless you want to cancel the INSERT / UPDATE.
Or you make it an AFTER trigger, which would work for this, too.
You need to reference NEW for the stream_id of the current row (which is automatically visible inside the trigger function.
If streams.comments can be NULL, use COALESCE.
And rather use unquoted, legal, lower-case identifiers.

How to add constraint to sql table so that table has exactly one row

Parameter table is initially created and one row is added in Postgres.
This table should have always one row, otherwise SQL queries using this table will produce incorrect results. DELETE or INSERT to this table are disallowed, only UPDATE is allowed.
How to add single row constraint to this table?
Maybe DELETE and INSERT triggers can raise an exception or is there simpler way?
The following will create a table where you can only insert one single row. Any update of the id column will result in an error, as will any insert with a different value than 42. The actual id value doesn't matter actually (unless there is some special meaning that you need).
create table singleton
(
id integer not null primary key default 42,
parameter_1 text,
parameter_2 text,
constraint only_one_row check (id = 42)
);
insert into singleton values (default);
To prevent deletes you can use a rule:
create or replace rule ignore_delete
AS on delete to singleton
do instead nothing;
You could also use a rule to make insert do nothing as well if you want to make an insert "fail" silently. Without the rule, an insert would generate an error. If you want a delete to generate an error as well, you would need to create a trigger that simply raises an exception.
Edit
If you want an error to be thrown for inserts or deletes, you need a trigger for that:
create table singleton
(
id integer not null primary key,
parameter_1 text,
parameter_2 text
);
insert into singleton (id) values (42);
create or replace function raise_error()
returns trigger
as
$body$
begin
RAISE EXCEPTION 'No changes allowed';
end;
$body$
language plpgsql;
create trigger singleton_trg
before insert or delete on singleton
for each statement execute procedure raise_error();
Note that you have to insert the single row before you create the trigger, otherwise you can't insert that row.
This will only partially work for a superuser or the owner of the table. Both have the privilege to drop or disable the trigger. But that is the nature of a superuser - he can do anything.
To make any table a singleton just add this column:
just_me bool NOT NULL DEFAULT TRUE UNIQUE CHECK (just_me)
This allows exactly one row. Plus add the trigger #a_horse provided.
But I would rather use a function instead of the table for this purpose. Simpler and cheaper.
CREATE OR REPLACE FUNCTION one_row()
RETURNS TABLE (company_id int, company text) LANGUAGE sql IMMUTABLE AS
$$SELECT 123, 'The Company'$$
ALTER FUNCTION one_row() OWNER TO postgres;
Set the owner to the user that should be allowed to change it.
Give a user permission to ALTER a function
Nobody else change it - except superusers of course. Superusers can do anything.
You can use this function just like you would use the table:
SELECT * FROM one_row();
If you need a "table", create a view (which is actually a special table internally):
CREATE VIEW one_row AS SELECT * FROM one_row();
I guess you will not use the PostgreSQL root user in your application so you could simply limit the permissions of your application user on UPDATE for this table.
An INSERT or DELETE will then cause an Insufficient privilege exception.