INSERT a number in a column based on other columns OLD INSERTs - postgresql

In PostgreSQL I have this table... (there is a primary key in the most left side "timestamp02" which is not shown in this image, please don't bother, its not important for the purpose of this question)
in the table above, all columns are entered via queries, except the "time_index" which I would like to be filled automatically via a trigger each time each row is filled.
This is the code to create the same table (without any value) so everyone could create it using the Postgre SQL query panel.
CREATE TABLE table_ebscb_spa_log02
(
pcnum smallint,
timestamp02 timestamp with time zone NOT NULL DEFAULT now(),
fn_name character varying,
"time" time without time zone,
time_elapse character varying,
time_type character varying,
time_index real,
CONSTRAINT table_ebscb_spa_log02_pkey PRIMARY KEY (timestamp02)
)
WITH (
OIDS=FALSE
);
ALTER TABLE table_ebscb_spa_log02
OWNER TO postgres;
What I would like the trigger to do is:
INSERT a number in the "time_index" column based on the INSERTed values of the "fn_name" and "time_type" columns in each row.
If both ("fn_name" and "time_type") do a combination (eg. Check Mails - Start) that doesn't exist in any row before (above), then INSERT 1 in the "time_index" column,
Elif both ("fn_name" and "time_type") do a combination that does exist in some row before (above), then INSERT the number following the one before(above) in the "time_index" column.
(pls look at the example table image, this trigger will produce every red highlighted square on it)
I have watch many, PostgreSQL tutorial videos, read many manuals, including these
http://www.postgresql.org/docs/9.4/static/sql-createtrigger.html
http://www.postgresql.org/docs/9.4/static/plpgsql-trigger.html
without any result.
I have tried so far this to create the function:
CREATE OR REPLACE FUNCTION on_ai_myTable() RETURNS TRIGGER AS $$
DECLARE
t_ix real;
n int;
BEGIN
IF NEW.time_type = 'Start' THEN
SELECT t.time_index FROM table_ebscb_spa_log02 t WHERE t.fn_name = NEW.fn_name AND t.time_type = 'Start' ORDER BY t.timestamp02 DESC LIMIT 1 INTO t_ix;
GET DIAGNOSTICS n = ROW_COUNT;
IF (n = 0) THEN
t_ix = 1;
ELSE
t_ix = t_ix + 1;
END IF;
END IF;
NEW.time_index = t_ix;
return NEW;
END
$$
LANGUAGE plpgsql;
And this to create the query:
CREATE TRIGGER on_ai_myTable
AFTER INSERT ON table_ebscb_spa_log02
FOR EACH ROW
EXECUTE PROCEDURE on_ai_myTable();
Then when I manually insert the values in the table, nothing change (no error message) time_index column just remain empty, what am I doing wrong???
Please some good PostgreSQL fellow programmer could give me a hand, I really have come to a death point in this task, I have any more ideas.
Thanks in advance

In an AFTER INSERT trigger, any changes you make to NEW.time_index will be ignored. The record is already inserted at this point; it's too late to modify it.
Create the trigger as BEFORE INSERT instead.

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;

record "new" has no field "cure" postgreSQL

so here's the thing,I have two tables: apointments(with a single p) and medical_folder and i get this
ERROR: record "new" has no field "cure"
CONTEXT: SQL statement "insert into medical_folder(id,"patient_AMKA",cure,drug_id)
values(new.id,new."patient_AMKA",new.cure,new.drug_id)"
PL/pgSQL function new_medical() line 3 at SQL statement
create trigger example_trigger after insert on apointments
for each row execute procedure new_medical();
create or replace function new_medical()
returns trigger as $$
begin
if apointments.diagnosis is not null then
insert into medical_folder(id,"patient_AMKA",cure,drug_id)
values(new.id,new."patient_AMKA",new.cure,new.drug_id);
return new;
end if;
end;
$$ language plpgsql;
insert into apointments(id,time,"patient_AMKA","doctor_AMKA",diagnosis)
values('30000','2017-05-24 0
07:42:15','4017954515276','6304745877947815701','M3504');
I have checked multiple times and all of my tables and columns are existing
Please help!Thank you!
Table structures are:
create table medical_folder (
id bigInt,
patient bigInt,
cure text,
drug_id bigInt);
create table apointments (
id bigint,
time timestamp without time zone,
"patient_AMKA" bigInt,
"doctor_AMKA" bigInt);
I was facing the same issue.
Change:
values(new.id,new."patient_AMKA",new.cure,new.drug_id);
to:
values(new.id,new."patient_AMKA",new."cure",new."drug_id");
This error means the table apointments (with 1 p) doesn't have a field named cure. The trigger occurs when inserting an apointment, so "new" is an apointment row. Maybe it is part of the diagnosis object?
The values for the second table are not available in the "new" row. You need a way to get and insert them, and using a trigger is not the easiest/clean way to go.
You can have your application do two inserts, one by table, and wrap them in a transaction to ensure they are both committed/rolled back. Another option, which lets you better enforce the data integrity, is to create a stored procedure that takes the values to be inserted in both tables and do the two inserts. You can go as far as forbidding user to write to the tables, effectively leaving the stored procedure the only way to insert the data.

Auto increment depending on value of column in PostgreSQL

The question follows, can I auto increment (verification_number) depending on a value of a specific column (here business_uuid) so that verification_number increments by one (1) depending on that business_uuid's own highest number of verification_number?
Database looks as follows:
table: verification
verification_id = integer, sequence (Primary Key)
business_uuid = text
verification_number = integer
The verification_id is the Primary Key in this table and I want the verification_number to follow it's own auto increment depending on what it's highest value is filtered only for business_uuid.
The business_uuid is a unique identifier for each business.
Is it possible?
I think that this is a bad design, but if you really want that, you can achieve it as follows:
Create a unique constraint:
ALTER TABLE verification
ADD CONSTRAINT verification_uuid_nr_unique
UNIQUE (business_uuid, verification_number);
The index created by this will also make the following trigger function faster.
Create a BEFORE trigger to modify verification_number:
CREATE OR REPLACE FUNCTION veritrig() RETURNS trigger
LANGUAGE plpgsql AS
$$BEGIN
SELECT COALESCE(max(verification_number)+1, 1) INTO NEW.verification_number
FROM verification
WHERE business_uuid = NEW.business_uuid;
RETURN NEW;
END;$$;
CREATE TRIGGER veritrig
BEFORE INSERT OR UPDATE ON verification FOR EACH ROW
EXECUTE PROCEDURE veritrig();
Insert new values like this:
INSERT INTO verification (business_uuid) VALUES ('42');
Then verification_number will be set as you desire.
There is a problem with concurrency, however.
If several sessions try to insert or update the table concurrently, you will get an error like this:
ERROR: duplicate key value violates unique constraint "verification_uuid_nr_unique"
DETAIL: Key (business_uuid, verification_number)=(43, 1) already exists.
That is because the SELECT statements in concurrent modifications will only see the current (committed) table contents and may erroneously try to insert the same verification_number for a business_uuid.
There is no way to avoid that short of locking the table. But if you receive such an error, you can simply retry the transaction, and odds are that it will work the next time.
It is not clear if you want the verification_number to be saved to the table but it is possible to create it at query time:
select verification_id, business_uuid,
row_number() over(
partition by business_uuid
order by verification_id
) as verification_number
from verification
The problem with the above approach is that the verification_number will change if rows are deleted
Window functions
CREATE OR REPLACE FUNCTION public.creaid(
IN key_field text,
IN table_mane text,
OUT id integer)
RETURNS integer AS
$BODY$
DECLARE maxid INT;
BEGIN
EXECUTE 'SELECT max('||key_field||') FROM '||table_name INTO maxid;
IF maxid IS NULL THEN
id = 1;
ELSE
id = maxid + 1;
END IF;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
Then, you can call that function as a default value of every key value of every table of your database, as follows:
DEFAULT creaid('key_field'::text, 'table_name'::text)

Capture columns in plpgsql during UPDATE

I am writing a trigger in plpgsql for Postgres 9.1. I need to be able to capture the column names that were issued in the SET clause of an UPDATE so I can record the specified action in an audit table. The examples in the Postgres documentation are simple and inadequate for my needs. I have searched the internet for days and I am unable to find any other examples that try to achieve what I want to do here.
I am on a tight schedule to resolve this soon. I don't know Tcl so pl/Tcl is out of the question for me at this point. pl/Perl may work but I don't know where to start with it. Also I wanted to find a way to accomplish this in pl/pgsql if at all possible for portability and maintenance. If someone can recommend a pl/Perl solution to this I would be grateful.
Here is the table structure of the target table that will be audited:
Note: There are many other columns in the record table but I have not listed them here in order to keep things simple. But the trigger should be able to record changes to any of the columns in the row.
CREATE TABLE record (
record_id integer NOT NULL PRIMARY KEY,
lastname text,
frstname text,
dob date,
created timestamp default NOW(),
created_by integer,
inactive boolean default false
);
create sequence record_record_id_seq;
alter table record alter record_id set default nextval('record_record_id_seq');
Here is my audit table:
CREATE TABLE record_audit (
id integer NOT NULL PRIMARY KEY,
operation char(1) NOT NULL, -- U, I or D
source_column text,
source_id integer,
old_value text,
new_value text,
created_date timestamp default now(),
created_by integer
);
create sequence record_audit_id_seq;
alter table record_audit alter id set default nextval('record_audit_id_seq');
My goal is to record INSERTS and UPDATES to the record table in the record_audit table that will detail not only what the target record_id was (source_id) that was updated and what column was updated (source_column), but also the old_value and the new_value of the column.
I understand that the column values will have to be CAST() to a type of text. I believe I can access the old_value and new_value by accessing NEW and OLD but I am having difficulty figuring out how to obtain the column names used in the SET clause of the UPDATE query. I need the trigger to add a new record to the record_audit table for every column specified in the SET clause. Note, there are not DELETE actions as records are simply UPDATED to inactive = 't' (and thus recorded in the audit table)
Here is my trigger so far (obviously incomplete). Please forgive me, I am learning pl/pgsql as I go.
-- Trigger function for record_audit table
CREATE OR REPLACE FUNCTION audit_record() RETURNS TRIGER AS $$
DECLARE
insert_table text;
ref_col text; --how to get the referenced column name??
BEGIN
--
-- Create a new row in record_audit depending on the operation (TG_OP)
--
IF (TG_OP = 'INSERT') THEN
-- old_value and new_value are meaningless for INSERTs. Just record the new ID.
INSERT INTO record_audit
(operation,source_id,created_by)
VALUES
('I', NEW.record_id, NEW.created_by);
ELSIF (TG_OP = 'UPDATE') THEN
FOR i in 1 .. TG_ARGV[0] LOOP
ref_col := TG_ARGV[i].column; -- I know .column doesn't exist but what to use?
INSERT INTO record_audit
(operation, source_column, source_id, old_value, new_value, created_by)
VALUES
('U', ref_col, NEW.record_id, OLD.ref_col, NEW.ref_col, NEW.created_by);
END LOOP;
END IF;
RETURN NULL; -- result is ignored anyway since this is an AFTER trigger
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER record_audit_trig
AFTER INSERT OR UPDATE on record
FOR EACH ROW EXECUTE PROCEDURE audit_record();
Thanks for reading this long and winding question!
you cannot to get this information - not in PL level - probably it is possible in C.
Good enough solution is based on changed fields in records NEW and OLD. You can get list of fields from system tables ~ are related to table that is joined to trigger.

Access to a row, which started a trigger

I know there're ways to access to query type, table names, oids etc. in trigger definitions:
http://www.postgresql.org/docs/8.3/static/plpgsql-trigger.html
Anyway, is there any chance that i run an UPDATE on a row, which started the trigger without actualy need to marking each row in the table by unique id?
e.g. if i have table for storing logs, i don't need any unique id here... and as the time goes there may actually appear some rows, which will be equal.
CREATE TABLE users_log
(
uid bigint NOT NULL,
event smallint NOT NULL,
source character varying,
event_time timestamp with time zone
)
I know, that due to microseconds precision in "timestamp with time zone" data type is this situation nearly impossible, but not really impossible...
So how shall i write the query in trigger to be able to UPDATE just the inserted row?
$BODY$BEGIN
UPDATE "users_log" SET "event_time" = now(); -- this updates all rows
-- WHERE "id" = NEW.id; - this is what i don't want
RETURN NEW;
END;$BODY$
You just use in your trigger:
$BODY$BEGIN
NEW.event_time = now();
RETURN NEW;
END;$BODY$
And it just works — no need for additional update. You need to declare this trigger as BEFORE TRIGGER though.