MySQL Trigger not entirely working - mysql

my trigger is coming back with no errors. But when i test it, no error message is returned and the test data is stored in the table.. Which isn't what I want.
Basically (this is for coursework), I want a trigger that displays an error message when a location other than Barcelona is inserted into the table.
This is my trigger. As I said, no errors come back, but it doesn't work?
DELIMITER $$
CREATE TRIGGER after_location_insert_finance
AFTER INSERT ON Finance
FOR EACH ROW
BEGIN
DECLARE LocationTrigger varchar(255);
DECLARE msg varchar(255);
SELECT Location INTO LocationTrigger
FROM Finance
WHERE Location != "Barcelona";
IF(LocationTrigger != "Barcelona") THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Not a valid location';
END IF;
END$$

The expression in the IF won't evaluate to TRUE if LocationTrigger is null.
And LocationTrigger will be NULL if there are not rows in Boss that satisfy the condition in the WHERE clause.
I'm not understanding why there's no check of the value being inserted into the location column; or why are we running a query of the Boss table, when we're comparing to literal value of 'Barcelona'.
Seems like we'd want this kind of check in a BEFORE trigger rather than an AFTER trigger.
Are we going to allow NULL values for location?
If the only value we allow to be inserted for location is 'Barcelona', and if we aren't allowing NULL values, then we don't need to mess with any query of the Boss table. Just do a comparison...
BEGIN
IF NOT ( NEW.location <=> 'Barcelona' ) THEN
SIGNAL ...
END IF;
END
Maybe the goal of the trigger is to enforce a referential integrity constraint (which we'd normally implement by declaring a FOREIGN KEY constraint, rather than implementing a procedure.)
Assuming that a "valid" location is defined by the domain of location values in Boss table,
and assuming we aren't going to allow NULL values, then:
BEGIN
DECLARE ls_location VARCHAR(255) DEFAULT NULL;
-- check if the value being inserted exists in a lookup table
SELECT b.location INTO ls_location
FROM Boss b
WHERE b.location = NEW.location
LIMIT 1
;
-- if we didn't find a match, ls_location will be NULL
IF ls_location IS NULL THEN
SIGNAL ...
END IF;
END
If no matching row is found in Boss, then ls_location will be NULL.
A "not equals" comparison to NULL is going to evaluate to NULL, not TRUE or FALSE.
Boolean logic is SQL is tri-valued... TRUE, FALSE and NULL
SELECT 0 = 1 AS c0
, 0 <> 1 AS c1
, 0 = NULL AS c2
, 0 <> NULL AS c3
, NULL = NULL AS c4
, NULL <> NULL AS c5

Related

MySQL: how to make an attribute "nullable" only if certain condition

I am creating a table for a DB, and I would like (if possible) to do something like this:
Attribute X can be NULL if, and only if, attribute Y is "value1".
Is there a way to do this? I want to do this because I could delete an entity, reducing the complexity of my project (or at least I think I would get some advantages).
Thanks :)
In very recent versions of MySQL, you can use a check constraint for this:
create table mytable (
x int,
y int,
check(x is not null or y = 1)
)
If MySQL version is not new enough for to use CHECK constraint (below 8.0.16) then use
DELIMITER ##;
CREATE TRIGGER tr_bi_check_my_constraint
BEFORE INSERT
ON my_table
FOR EACH ROW
BEGIN
IF NEW.attribute_X IS NULL AND NEW.attribute_Y != 'value1' THEN
SIGNAL SQLSTATE 45000
SET MESSAGE_TEXT = 'Attribute X can be NULL if, and only if, attribute Y is "value1".';
END IF;
END
##;
CREATE TRIGGER tr_bu_check_my_constraint
BEFORE UPDATE
ON my_table
FOR EACH ROW
BEGIN
IF NEW.attribute_X IS NULL AND NEW.attribute_Y != 'value1' THEN
SIGNAL SQLSTATE 45000
SET MESSAGE_TEXT = 'Attribute X can be NULL if, and only if, attribute Y is "value1".';
END IF;
END
##;
DELIMITER ;

Boolean variable always assuming 1 in select...into - MySQL

I'm doing a stored procedure to update a table and that table has a boolean named: "Finished", as a field. This field informs us if a game is finished. In my problem it makes sense to be able to set something as finished before the expiration date so, because of that, I'm checking if the row to update has the "Finished" field as true or if the expiration date has passed.
SET #isFinished=0;
SELECT Finished INTO #isFinished FROM game WHERE ID = gameID;
-- gameID comes as a parameter
-- date comes as a parameter as well
IF DATEDIFF(STR_TO_DATE(date, "%Y-%m-%d") , CURDATE()) < 0 OR #isFinished<>0 THEN
select CONCAT("Game can't be updated because it's already finished. Days missing:",DATEDIFF( STR_TO_DATE(date, "%Y-%m-%d"), CURDATE() )," and #finished=", #isFinished, ", game=",gameID)
into #msg;
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = #msg;
END IF;
The problem is that when I try to update an unfinished game it is getting into the if and throwing the error message:
"sqlMessage: 'Game can\'t be updated because it\'s already finished. Days missing:337 and #finished=1, game=2'".
quick note: The variable #isFinished is never used but in this block of code.
I've always assured that the value of "Finished" was 0 before I tested it and yet it keeps selecting it as 1 and, because of that, getting into the if.
I thought it could be from the select...into so I tried it out of the stored procedure (literally copy paste, just changed the "gameID" to the actual ID that I'm using) and it worked perfectly.
SELECT Finished INTO #isFinished FROM game WHERE ID = 2;
SELECT #isFinished
After this, I don't know what more can I check. If anyone could help I'd be thankful.
Isolated test:
create database test;
use test;
create table Tournament(
ID int(10) not null unique auto_increment,
Name varchar(250) not null,
Start_Date date not null,
End_Date date not null,
Primary key(ID)
);
create table Game(
ID int(10) not null unique auto_increment,
Tournament_ID int(10),
Date date,
Finished boolean,
Foreign Key(Tournament_ID) references Tournament(ID) ON DELETE CASCADE,
Primary Key(ID)
);
INSERT INTO Tournament VALUES(NULL, "tournament1", str_to_date("2020-06-01","%Y-%m-%d"), str_to_date("2020-07-01","%Y-%m-%d"));
INSERT INTO Game VALUES(NULL, 1, str_to_date("2020-06-02","%Y-%m-%d"), 0);
DELIMITER $$
DROP PROCEDURE IF EXISTS `UpdateGame`$$
CREATE PROCEDURE `UpdateGame`(IN gameID int, date varchar(10), finished boolean)
BEGIN
SET #isFinished=0;
SELECT Finished INTO #isFinished FROM game WHERE ID = gameID;
IF DATEDIFF(STR_TO_DATE(date, "%Y-%m-%d") , CURDATE()) < 0 OR #isFinished<>0 THEN
select CONCAT("Game can't be updated because it's already finished. Days missing:",DATEDIFF( STR_TO_DATE(date, "%Y-%m-%d"), CURDATE() )," and #finished=", #isFinished, ", game=",gameID)
into #msg;
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = #msg;
END IF;
UPDATE game SET Date=STR_TO_DATE(date, "%Y-%m-%d"), Finished=finished WHERE ID=gameID;
END$$
call UpdateGame(1,"2020-06-03",1);
SELECT * FROM game;
SELECT Finished INTO #isFinished FROM game WHERE ID = 1;
SELECT #isFinished;
You have IN param Finished so inside your stored procedure, when querying the table, in fact you querying the IN param.
So you need to delete/rename the Finished IN param or write your internal select query as:
SELECT `game`.`Finished`
INTO #isFinished
FROM game
WHERE ID = gameID;
P.S. People usually have some kind of prefixes on input params to distinguish them visually inside the stored procedure from tables, columns of local variables.

Why opposite operation <> doesn't work on trigger?

I have a trigger which is AFTER UPDATE. I have a condition in that which doesn't work, How can I fix it?
$$
BEGIN
IF NEW.colname <> OLD.colname THEN
DELETE FROM tableX WHERE id = OLD.id;
END IF;
END
$$
It should be noted if I write this IF 1 THEN then that DELETE query works. So the problem is that condition. What's wrong with it?
As #Darwin von Corax said in the comments, when one of these NEW.colname, OLD.colname is null, then all of that condition returns false. So I want to know how can I create a condition which acts like this ?
Null <> 10 -- true
Null <> Null -- false
10 <> 12 -- true
32 <> Null -- true
3 <> 3 -- false
If either NEW.colname or OLD.colname is null, then <> will return null which if would treat the same as false.
Fortunately, there is a solution: the <=> (NULL-safe equality) operator, which returns 1 if both operands are null and 0 if only one is. The expression
IF NOT (NEW.colname <=> OLD.colname) THEN
should do what you need.
Just add logic for checking null conditions. It is not triggered when both columns are null which means that no changes happen.
$$
BEGIN
IF
-- when exactly one of the column is null
NEW.colname is not null and OLD.colname is null or
NEW.colname is null and OLD.colname is not null or
-- when both are not null compare their values
NEW.colname is not null and OLD.colname is not null and NEW.colname <> OLD.colname THEN
DELETE FROM tableX WHERE id = OLD.id;
END IF;
END
$$

Check if a column is null or not before inserting another record in SQL

I need to check first if the EndTime column in my table is null or not before I can insert another record. If the Endtime column is not null than a new record can be inserted else an error must be thrown. I'm not sure how to create the error in SQL.
This is what I tried but it doesn't work
ALTER PROCEDURE [dbo].[AddDowntimeEventStartByDepartmentID]
(#DepartmentId int,
#CategoryId int,
#StartTime datetime,
#Comment varchar(100) = NULL)
AS
BEGIN TRY
PRINT N'Starting execution'
SET #StartTime = COALESCE(#StartTime, CURRENT_TIMESTAMP);
INSERT INTO DowntimeEvent(DepartmentId, CategoryId, StartTime, EndTime, Comment)
WHERE EndTime = NULL
OUTPUT
inserted.EventId, inserted.DepartmentId,
inserted.CategoryId, inserted.StartTime,
inserted.EndTime, inserted.Comment
VALUES(#DepartmentId, #CategoryId, #StartTime, NULL, #Comment)
END TRY
BEGIN CATCH
SELECT ERROR_NUMBER(),ERROR_MESSAGE()
END CATCH
Here is my table:
CREATE TABLE [dbo].[DowntimeEvent](
[EventId] [int] IDENTITY(0,1) NOT NULL,
[DepartmentId] [int] NOT NULL,
[CategoryId] [int] NOT NULL,
[StartTime] [datetime] NOT NULL,
[EndTime] [datetime] NULL,
[Comment] [varchar](100) NULL,
)
You could use the INSERT...SELECT syntax instead of INSERT...VALUES to be able to use a WHERE clause (with a different condition to the one you tried to use, see below), then check the number of affected rows and raise an error if it is 0:
...
BEGIN TRY
...
INSERT INTO DowntimeEvent
...
SELECT #DepartmentId, #CategoryId, #StartTime, NULL, #Comment
WHERE NOT EXISTS (
SELECT *
FROM dbo.DowntimeEvent
WHERE DepartmentId = #DepartmentId
AND CategoryId = #CategoryId
AND EndTime IS NULL
);
IF ##ROWCOUNT = 0
RAISERROR ('A NULL row already exists!', 16, 1)
;
END TRY
BEGIN CATCH
...
END CATCH;
(Of course, you will need to omit your WHERE clause as invalid Transact-SQL.)
If you want a prevention mechanism at the database level rather than just in your stored procedure, so as to be able to prevent invalid additions from any caller, you may want to consider a trigger.
A FOR INSERT trigger like this would check if new rows violate the rule "Do not add rows newer than the existing NULL row" (as well as "Do not add older rows with empty EndTime") and roll back the transaction if they do:
CREATE TRIGGER DowntimeEvent_CheckNew
ON dbo.DowntimeEvent
FOR INSERT, UPDATE
-- do nothing if EndTime is not affected
IF NOT UPDATE(EndTime)
RETURN
;
-- raise an error if there is an inserted NULL row
-- older than another existing or inserted row
IF EXISTS (
SELECT *
FROM dbo.DowntimeEvent AS t
WHERE t.EndTime IS NULL
AND EXISTS (
SELECT *
FROM inserted AS i
WHERE i.DepartmentId = t.DepartmentId
AND i.CategoryId = t.CategoryId
AND i.StartTime >= t.StartTime
)
)
BEGIN
RAISERROR ("An attempt to insert an older NULL row!", 16, 1);
ROLLBACK TRANSACTION;
END;
-- raise an error if there is an inserted row newer
-- than the existing NULL row or an inserted NULL row
IF EXISTS (
SELECT *
FROM inserted AS i
WHERE i.EndTime IS NULL
AND EXISTS (
SELECT *
FROM dbo.DowntimeEvent AS t
WHERE t.DepartmentId = i.DepartmentId
AND t.CategoryId = i.CategoryId
AND t.StartTime >= i.StartTime
)
)
BEGIN
RAISERROR ("An older NULL row exists!", 16, 1);
ROLLBACK TRANSACTION;
END;
Note that merely issuing ROLLBACK TRANSACTION in a trigger already implies raising a level 16 error like this:
Msg 3609, Level 16, State 1, Line nnn
The transaction ended in the trigger. The batch has been aborted.
so, you may not need your own. There would be a difference in the meaning of Line nnn between the message above and the one brought by your own RAISERROR, however: the line number in the former would refer to the location of the triggering statement, whereas the line number in the latter would refer to a position in your trigger.

IF NOT EXISTS in trigger

I have tow tables concept_access and concept_access_log. I want to create a trigger that works every time something is deleted from concept_access, check if there is similar record in log table and if not, inserts new one before it is deleted from concept_access.
I modified trigger and now it looks like this:
DROP TRIGGER IF EXISTS before_delete_concept_access;
DELIMITER //
CREATE TRIGGER before_delete_concept_access
BEFORE DELETE ON `concept_access` FOR EACH ROW
BEGIN
IF (SELECT 1 FROM concept_access_log WHERE map=OLD.map
AND accesstype=OLD.accesstype AND startdate=OLD.startdate AND stopdate=OLD.stopdate) IS NULL THEN
INSERT INTO concept_access_log (map, accesstype, startdate, stopdate)
VALUES (OLD.map, OLD.accesstype, OLD.startdate, OLD.stopdate);
END IF;
END//
DELIMITER ;
Sample data in concept_access before delete:
map accesstype startdate stopdate
1 public NULL NULL
1 loggedin 2011-05-11 NULL
1 friends NULL NULL
Log table already has first 2 rows. And they are exactly the same as in concept_access. When I delete first row from concept_access table, I get this in log table:
map accesstype startdate stopdate
1 public NULL NULL
1 loggedin 2011-05-11 NULL
1 friends NULL NULL
1 public NULL NULL
While it is not supposed to insert anything because (1,public,null,null) already exists there.
This table has no primary key. I was not creating structure, so don't ask me why. Changing it will ruin a lot of already existing functionality. I just need to keep log of what was removed from table concept_access and store it in log without duplicates.
I would really appreciate, if anyone can figure out what is going wrong.
DROP TRIGGER IF EXISTS before_delete_concept_access;
DELIMITER //
CREATE TRIGGER before_delete_concept_access
BEFORE DELETE ON `concept_access` FOR EACH ROW
BEGIN
IF (SELECT COUNT(*) FROM concept_access_log WHERE map=OLD.map
AND accesstype=OLD.accesstype AND startdate=OLD.startdate AND stopdate=OLD.stopdate) = 0 THEN
INSERT INTO concept_access_log (map, accesstype, startdate, stopdate)
VALUES (OLD.map, OLD.accesstype, OLD.startdate, OLD.stopdate);
END IF;
END//
DELIMITER ;
I am not using not exists, just test if the match count greater than 0
your code runs well on my machine, what's your MySQL version?
mysql> select version();
+------------+
| version() |
+------------+
| 5.1.56-log |
+------------+
1 row in set (0.00 sec)
Instead of this
... WHERE map = OLD.map ...
-- map OLD.map bool
-- --- ------- ----
-- 'a' 'a' TRUE
-- 'a' 'b' FALSE
-- 'a' NULL FALSE
-- NULL 'b' FALSE
-- NULL NULL FALSE <-- Undesired! We want this to be TRUE
try <=>, which is MySQL's answer to the SQL standard IS (NOT) DISTINCT FROM predicate:
... WHERE map <=> OLD.map ...
-- TRUE when both have the same non-NULL value or when both are NULL
-- Equivalents:
-- WHERE (map = OLD.map OR (map IS NULL AND OLD.map IS NULL))
-- WHERE (map = OLD.map OR COALESCE(map, OLD.map) IS NULL)