mysql Trigger for logging, find changed columns - mysql

I am writing a trigger to keep track of all the changes that happens in a table. Unfortunately the table has 150+ columns and I wanted to avoid writing each column in the code (Ex. new.col1, new.col2....) and thus I wrote a following query in "after update trigger"
INSERT INTO logs SELECT *, NOW() FROM abc WHERE abc.id = NEW.Id;
This idea is causing multiple issue due to duplication of data that is not changed in update query.
In a nutshell I want to dynamically find out which columns were part of the update query and if that is not possible is there a way to iterate through all the columns of "new" row so I can dynamically compare old.#colName == new.#colName?
I have already seen
Oracle PL/SQL: Loop Over Trigger Columns Dynamically, How to determine if anything changed in update trigger in t-sql and MySQL UPDATE trigger: INSERTing the values of the columns that actually changed.
The last link is the closes to what I need with only one difference, I don't want to hard code column names in following statment because I have way over 100+ columns in all the tables I am going to write similar trigger for!!
IF NEW.column1 <> OLD.column1 THEN INSERT INTO... END IF; IF NEW.column2 <> OLD.column2 THEN INSERT INTO... END IF

I've been doing a bit of research on this this morning and looks like I have come across much of the same search results as you. Ultimately it looks to me like there's no way to loop over all table columns and reference the corresponding old/new values. I'm settling on explicitly checking each column and then logging:
IF (NEW.fld1 <> OLD.fld1) OR (NEW.fld1 IS NOT NULL AND OLD.fld1 IS NULL) OR (NEW.fld1 IS NULL AND OLD.fld1 IS NOT NULL) THEN
INSERT INTO `fld_audit` (`table`, `fldname`, `oldval`, `newval`)
VALUES ("tblname", "fld1", OLD.fld1, NEW.fld1);
END IF;
IF (NEW.fld2 <> OLD.fld2) OR (NEW.fld2 IS NOT NULL AND OLD.fld2 IS NULL) OR (NEW.fld2 IS NULL AND OLD.fld2 IS NOT NULL) THEN
INSERT INTO `fld_audit` (`table`, `fldname`, `oldval`, `newval`)
VALUES ("tblname", "fld2", OLD.fld2, NEW.fld2);
END IF; ...
I found an inkling of another solution here. In theory you could have 3 delimited lists, one for column names, one for old vals and one for new vals. You would have to explicitly reference the old and new vals, but that would be one line (easier to maintain or copy/paste to implement on other tables) and you could then loop. So in pseudo code it would look something like this:
fields_array = concat_ws(",", "fld1", "fld2");
old_vals_array = concat_ws(",", OLD.fld1, OLD.fld2);
new_vals_array = concat_ws(",", NEW.fld1, NEW.fld2);
foreach fields_array as key => field_name
INSERT INTO `fld_audit` (`table`, `fldname`, `oldval`, `newval`)
VALUES ("tblname", field_name, old_vals_array[key], vew_vals_array[key]);
I haven't thought this through too much. You might need to call into a stored procedure rather than set variables. But it might be worth looking into. I've spent enough time on my triggers already. Not sure I could validate (to my boss) trial and error time on a more elegant solution.

As ingratiatednerd already suggested, you can use CONCAT_WS to make strings out of all required values and make a single compare statement.
Perhaps the following is useful to someone:
DECLARE old_concat, new_concat text;
SET old_concat = CONCAT_WS(',', OLD.fld1, OLD.fld2, ...);
SET new_concat = CONCAT_WS(',', NEW.fld1, NEW.fld2, ...);
IF old_concat <> new_concat
THEN
INSERT STATEMENT
END IF;

Related

Get name of updated column in history table with update trigger (Mysql) [duplicate]

I am writing a trigger to keep track of all the changes that happens in a table. Unfortunately the table has 150+ columns and I wanted to avoid writing each column in the code (Ex. new.col1, new.col2....) and thus I wrote a following query in "after update trigger"
INSERT INTO logs SELECT *, NOW() FROM abc WHERE abc.id = NEW.Id;
This idea is causing multiple issue due to duplication of data that is not changed in update query.
In a nutshell I want to dynamically find out which columns were part of the update query and if that is not possible is there a way to iterate through all the columns of "new" row so I can dynamically compare old.#colName == new.#colName?
I have already seen
Oracle PL/SQL: Loop Over Trigger Columns Dynamically, How to determine if anything changed in update trigger in t-sql and MySQL UPDATE trigger: INSERTing the values of the columns that actually changed.
The last link is the closes to what I need with only one difference, I don't want to hard code column names in following statment because I have way over 100+ columns in all the tables I am going to write similar trigger for!!
IF NEW.column1 <> OLD.column1 THEN INSERT INTO... END IF; IF NEW.column2 <> OLD.column2 THEN INSERT INTO... END IF
I've been doing a bit of research on this this morning and looks like I have come across much of the same search results as you. Ultimately it looks to me like there's no way to loop over all table columns and reference the corresponding old/new values. I'm settling on explicitly checking each column and then logging:
IF (NEW.fld1 <> OLD.fld1) OR (NEW.fld1 IS NOT NULL AND OLD.fld1 IS NULL) OR (NEW.fld1 IS NULL AND OLD.fld1 IS NOT NULL) THEN
INSERT INTO `fld_audit` (`table`, `fldname`, `oldval`, `newval`)
VALUES ("tblname", "fld1", OLD.fld1, NEW.fld1);
END IF;
IF (NEW.fld2 <> OLD.fld2) OR (NEW.fld2 IS NOT NULL AND OLD.fld2 IS NULL) OR (NEW.fld2 IS NULL AND OLD.fld2 IS NOT NULL) THEN
INSERT INTO `fld_audit` (`table`, `fldname`, `oldval`, `newval`)
VALUES ("tblname", "fld2", OLD.fld2, NEW.fld2);
END IF; ...
I found an inkling of another solution here. In theory you could have 3 delimited lists, one for column names, one for old vals and one for new vals. You would have to explicitly reference the old and new vals, but that would be one line (easier to maintain or copy/paste to implement on other tables) and you could then loop. So in pseudo code it would look something like this:
fields_array = concat_ws(",", "fld1", "fld2");
old_vals_array = concat_ws(",", OLD.fld1, OLD.fld2);
new_vals_array = concat_ws(",", NEW.fld1, NEW.fld2);
foreach fields_array as key => field_name
INSERT INTO `fld_audit` (`table`, `fldname`, `oldval`, `newval`)
VALUES ("tblname", field_name, old_vals_array[key], vew_vals_array[key]);
I haven't thought this through too much. You might need to call into a stored procedure rather than set variables. But it might be worth looking into. I've spent enough time on my triggers already. Not sure I could validate (to my boss) trial and error time on a more elegant solution.
As ingratiatednerd already suggested, you can use CONCAT_WS to make strings out of all required values and make a single compare statement.
Perhaps the following is useful to someone:
DECLARE old_concat, new_concat text;
SET old_concat = CONCAT_WS(',', OLD.fld1, OLD.fld2, ...);
SET new_concat = CONCAT_WS(',', NEW.fld1, NEW.fld2, ...);
IF old_concat <> new_concat
THEN
INSERT STATEMENT
END IF;

Trigger is preventing an insert

I have a trigger running on my Asterisk "cel" table with a bunch of conditions. It appears that this condition prevents the record I am trying to catch from being inserted into the "cel" table.
Here is the trigger's condition:
IF (NEW.eventtype = 'CHAN_START' AND CHAR_LENGTH(NEW.cid_num) > 4 AND NEW.cid_ani = '' AND NEW.cid_rdnis = '' AND NEW.cid_dnid = '' AND CHAR_LENGTH(NEW.exten) > 4) THEN
INSERT INTO mem_callers VALUES (NEW.linkedid, NEW.eventtype, NEW.cid_num, NEW.exten, NEW.exten, NEW.eventtime);
END IF;
This is supposed to insert a record in my "mem_callers" table, but with this condition in my trigger, the "CHAN_START" record is never inserted into the "cel" table.
What am I doing wrong here?
Ok, it appears to be misbehaving because I'm doing something stupid.
I added another column to the "mem_callers" table earlier and my INSERT statement in that condition did not include a value for the new column.
Once I've added a value both tables inserted the correct records.

Dynamic value selection on INSERT statement

So my problem is the following. I've got a timeStatus column that will have one of two values on an INSERT statement, 'pending' or 'never', depending on whether the column fromDate is NULL or not.
I've made this prepared statement that doesn't work but represents what I intend. On the other hand I'm not sure if a constraint would be in order here, rather then having it specified on the statement. This way I could specify the status value for an insert or update and the table would know what to do. However I need some guidance as to what method to go with and where to go to learn it.
Here's the statement:
INSERT INTO Bservices (
servStatus, timeStatus,
fromDetails, fromDate, fromTime)
VALUES(
'pending', IF(ISNULL(`fromDate`)) 'pending' ELSE 'never',
'a', '', '')
The intended behavior is the following:
ON INSERT
if(fromDate == '') {
timeStatus = 'pending'
} else {
timeStatus = 'never'
}
ON UPDATE
if(timeStatus == 'pending' && fromDate != '') {
timeStatus = 'updated'
}
This doesn't work when you do it with expressions in the VALUES clause of an INSERT statement, because the expressions are evaluated before the row has been created. Therefore all columns are naturally NULL.
To do what you want, you need to write triggers BEFORE INSERT and BEFORE UPDATE. Something like the following, though I have not tested this so I'll leave debugging up to you:
CREATE TRIGGER insBservices
BEFORE INSERT ON Bservices
FOR EACH ROW
SET NEW.timeStatus = IF(NEW.fromDate IS NULL, 'pending', 'never');
CREATE TRIGGER updBservices
BEFORE UPDATE ON Bservices
FOR EACH ROW
SET NEW.timeStatus = IF(NEW.fromDate IS NOT NULL AND OLD.timeStatus = 'pending',
'updated', NEW.timeStatus);
Re your comment:
If you want to learn more about triggers, the MySQL manual is actually pretty weak in this area. They show you the syntax for reference, but not many examples. There are a lot of tricky parts.
For example, understanding when to use DELIMITER when you define triggers, to account for the ambiguity between semicolons inside the body of a trigger, versus the terminator of the CREATE TRIGGER statement itself. This applies to CREATE PROCEDURE and CREATE FUNCTION as well.
I wrote an example and an explanation in my answer to Create function through MySQLdb.
There are tutorials on triggers, for example:
http://code.tutsplus.com/articles/introduction-to-mysql-triggers--net-12226
http://www.mysqltutorial.org/mysql-triggers.aspx

MySql Basic table creation/handing

I'm trying to create a simple table where I insert field and I do some checks in MySql. I've used Microsoft SQL relatively easy. Instead, MySql give evrrytime query errors without even specifying what's going on. Poor MySql software design apart, here's what I'm trying to do:
1 table with 4 fields with an autoincremental autogenerated number to det an ID as primary key
CREATE TABLE `my_db`.`Patients_table` (
`ID_Patient` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY ,
`Patient_name` VARCHAR( 200 ) NOT NULL ,
`Recovery_Date` DATETIME NOT NULL ,
`Recovery_count` INT NOT NULL
) ENGINE = MYISAM
a simple stored procedure to insert such fields and check if something exist before inserting:
CREATE PROCEDURE nameInsert(IN nome, IN data)
INSERT INTO Patients_table (Patient_name,Recovery_Date) values (nome,data)
IF (EXISTS (SELECT Recovery_count FROM Tabella_nomi) = 0) THEN
INSERT INTO (Patients_table (Recovery_count)
ELSE
SET Recovery_count = select Recovery_count+1 from Patients_table
END
this seems wrong on many levels and MySQL useless syntax checker does not help.
How can I do this? Thanks.
There seems to be a lot wrong with this block of code. (No offense intended!)
First, Procedures need to be wrapped with BEGIN and END:
CREATE PROCEDURE nameInsert(IN nome, IN data)
BEGIN
...[actually do stuff here]
END
Second, since your table is declared with all fields as NOT NULL, you must insert all fields with an INSERT statement (this includes the Recovery_Date column, and excludes the AUTO_INCREMENT column). You can add DEFAULT CURRENT_TIMESTAMP to the date column if you want it to be set automatically.
INSERT INTO Patients_table (Patient_name,Recovery_Date) values (nome,data)
Third, what exactly is your IF predicate doing?
EXISTS (SELECT Recovery_count FROM Tabella_nomi) = 0
If you want to check if a row exists, don't put the = 0 at the end. Also, Tabella_nomi isn't declared anywhere in that procedure. Also, your SELECT statement should have a WHERE clause, since I'm assuming you want to select a specific row (this is going to select a result set of all recovery_counts).
Fourth, the second INSERT statement seems a little messy. It should look more like the first INSERT, and keep the point I made above in mind.
INSERT INTO (Patients_table (Recovery_count)
Fifth, the ELSE statement
SET Recovery_count = select Recovery_count+1 from Patients_table
Has some problems too. SET is meant for setting variables, not values in rows. I'm not 100% sure what your intent is from this statement, but it looks like you meant to increment the Recovery_count column of a certain row if it already exists. In which case, you meant to do something like this:
UPDATE Patients_table SET Recovery_count = Recovery_count+1 WHERE <conditional predicate>
Where the conditional predicate is something like this:
Patients_name = nome
Try these things, and look at the errors it gives you when you try to execute the CREATE STATEMENT. I bet they're more useful then you think!

Trigger can't work properly when check condition

I just want to store records in two way.
If it exists, it will update information
If it dosn't exist, it will insert as new record
Here are the outline schemas for my tables:
product_purchase_item has product_purchase_item_id, product_id, quantity columns
product_stock has product_stock_id, product_id, product_total_quantity columns
I was creating a trigger
DELIMITER //
CREATE TRIGGER store_check AFTER INSERT ON product_purchase_item
FOR EACH ROW
BEGIN
DECLARE X INTEGER;
SET X =(SELECT product_id FROM product_stock where product_id = NEW.product_id);
IF NEW.product_id !=X THEN
INSERT INTO product_stock VALUES(NULL,NEW.product_id,NEW.quantity);
ELSE
UPDATE product_stock
SET product_total_quantity=product_total_quantity+NEW.quantity
WHERE product_id=NEW.product_id;
END IF;
END;//
DELIMITER;//
The problem is in product_stock table, INSERT query doesn't work when product record not exist, but update query work does properly when product record does exist.
Is it really the INSERT statement that "doesn't work"? Or, is the problem that the INSERT statement is not being executed at all?
What happens when the preceding SELECT query does not return a row? What value gets assigned to X?
When X has that value, does a conditional test "foo != X" return TRUE, or does it return something else (like FALSE, or NULL)?
Have you tried something like this?
IF NEW.product_ID = X THEN
UPDATE ...
ELSE
INSERT ...
END IF;
(I know it's bad practice to answer a question with a question; but it seemed apropros, since answering those will get you the answer to the question you really wanted to ask.)
You can do the desired operation through one of MySQL's standard commands:
INSERT INTO product_stock (product_id,product_total_quantity)
VALUES (NEW.product_id,NEW.quantity)
ON DUPLICATE KEY UPDATE product_total_quantity=product_total_quantity+NEW.quantity;
see the MySQL manual