Ok so I'm making this little excercise in MySQL. I'm a pokemon trainer that needs to have a record of wins and loses, who I battled and the money earned. Everything goes all dandy until I get to this part:
*Create a function that calculates the earnings, if the battle was won then the earnings increase, if the battle was a loss then earnings decrease.
I assign the rival trainers with a "class" from 1 to 4, each trainer gives a certain amount of money
1.- 250
2.- 500
3.- 1000
4.- 2000
My tables structures go more or less like this:
battle(idBattle, idTrainer, outcome, date, place)
Trainer(idTrainer, nameTrainer, class)
protagonist(idProtagonist, loses, wons, earnings)
I need to check on the outcome of the battle to know if my earnings will increase or decrease, then go to my table Trainer and check the class to see how much is the class worth, then return how much will I increase or decrease my earnings.
Also I want to create a trigger that automatically updates my table(ie. the main trainers table) when a new win or loss has been entered as well as updating the earnings. I'll leave the code bellow, I'm all dried out and have no idea what to do now. I'm pretty much desperate. I may have been looking at this problem from the worng angle, just need some advise.
MANY, MANY THANKS IN ADVANCE IF YOU CAN HELP ME OUT!
CREATE DATABASE ricardoRifa;
USE ricardoRifa;
CREATE TABLE entrenador (//trainer table
idEntrenador INT NOT NULL DEFAULT 98000,
nombreEntrenador VARCHAR(20),
clasificacion INT NOT NULL DEFAULT 1, //this is the trainer class
PRIMARY KEY (idEntrenador)
) ENGINE=INNODB;
CREATE TABLE batalla (
idBatalla INT NOT NULL AUTO_INCREMENT,
idEntrenador INT,
resultado BOOLEAN, //false=lost battle, true=won
lugar VARCHAR(20),
fecha DATE,
PRIMARY KEY (idBatalla , idEntrenador),
CONSTRAINT fk_idEntrenador FOREIGN KEY (idEntrenador)
REFERENCES entrenador (idEntrenador)
ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE=INNODB;
CREATE TABLE ricardo (//main trainer table, I wanted it to be like this for there may be more than one instance depending on the main trainer id
idRicardo INT NOT NULL DEFAULT 11490677,
perdidas INT NOT NULL DEFAULT 0,
victorias INT NOT NULL DEFAULT 0,
ganancias INT NOT NULL DEFAULT 10000,
PRIMARY KEY (idRicardo)
) ENGINE=INNODB;
delimiter |
CREATE PROCEDURE incersionEntrenador(idEntrenador INT, nombreEntrenador VARCHAR(20), clasificacion INT)
BEGIN
INSERT INTO entrenador VALUES(idEntrenador, nombreEntrenador, clasificacion);
END |
delimiter |
CREATE PROCEDURE incersionBatalla(idEntrenador INT, resultado BOOLEAN, lugar VARCHAR(20), fecha DATE)
BEGIN
INSERT INTO batalla (idEntrenador, resultado, lugar, fecha) VALUES(idEntrenador, resultado, lugar, fecha);
END |
delimiter |
CREATE FUNCTION calcularGanancia() RETURNS INT
BEGIN
DECLARE class, dinero INT;
SELECT
resultado, clasificacion
INTO class FROM
batalla
INNER JOIN
entrenador USING (idEntrenador)
WHERE
batalla.idEntrenador = entrenador.idEntrenador;
CASE class
WHEN 1 THEN SET dinero=250;
WHEN 2 THEN SET dinero=500;
WHEN 3 THEN SET dinero=1000;
WHEN 4 THEN SET dinero=2000;
ELSE SET dinero=0;
END CASE;
IF resultado = TRUE
THEN SET dinero = dinero * 1;
ELSE
SET dinero = dinero * -1;
END IF;
RETURN dinero;
END |
delimiter |
CREATE TRIGGER ricardoUpdate BEFORE
INSERT ON batalla
FOR EACH ROW BEGIN
UPDATE ricardo
SET perdidas=(SELECT COUNT(resultado) FROM batalla WHERE resultado=FALSE),
victorias=(SELECT COUNT(resultado) FROM batalla WHERE resultado=TRUE),
ganancias= ganancias + (SELECT CALCULARGANANCIA());
END |
I get this error with my function, Error Code 1222: The used SELECT statements have a different number of columns.
[edited]
That error usually means you have to explicitly pick the columns of your joined tables. Try changing your select in your calcularGanancia() function to something like the following (note I used aliases bat and ent):
declare class, resultado, dinero INT; /* note resultado here and into clause */
select bat.resultado, ent.clasificacion
into resultado, class
from batalla bat inner join
entrenador ent
ON bat.idEntrenador = ent.idEntrenador;
Related
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.
I have created these two tables:
CREATE TABLE oferta
(
id_agentie INT,
id_spatiu INT,
vanzare CHAR(1),
pret INT,
moneda CHAR(5),
CONSTRAINT pk_oferta PRIMARY KEY(id_agentie, id_spatiu)
);
CREATE TABLE Spatiu
(
id_spatiu INT PRIMARY KEY,
adresa VARCHAR(45),
zona INT,
suprafata INT,
id_tip INT
);
I need to create a trigger that, whenever I insert a new 'pret'.If the value of 'pret' is less than 2 * 'suprafata', then I'd like to change the value of 'pret' into 2 * 'suprafata' and the value of 'moneda' into 'EUR'.
I have tried, but I couldn't manage to get it done.
EDIT: I am using MySql. Sorry for not specifying.
Here is a code snippet that should anwser your need.
The trigger will run before each insert on oferta. It will first run a query to recover the value of suprafata in the corresponding record of table spatiu, then compare it to the pret value passed to the insert order. When pret is (strictly) less than twice suprafata, the trigger modifies the values of pret and moneda.
DELIMITER //
CREATE TRIGGER my_trigger
BEFORE INSERT ON oferta
FOR EACH ROW
BEGIN
DECLARE v1 INT;
SELECT suprafata INTO v1 WHERE id_spatiu = NEW.id_spatiu;
IF ( NEW.pret < 2 * v1) THEN
NEW.pret = 2 * v1;
NEW.moneda = 'EUR';
END IF
END//
DELIMITER ;
I store in my DB the demands some users can do. The demands can have differents status (stored as events), such as in progress, finished, waiting, and so on (there's 30ish differents status). The demands have differents deadlines corresponding of differents steps of the treatment.
I need to "freeze" some deadlines of the demands, if their current status belongs to a list of pre-defined ones.
In example :
If a demand has the status "A", I have to "freeze" the deadline 2 to 5.
If the status is "B" or "C", I have to "freeze" the deadline 3 to 5.
If the status is "D", I have to "freeze" the deadline 4 and 5.
I plan to use an EVENT that runs every day, at 19:00 to update (add 1 day) the differents deadlines of the concerned demands.
Table structures :
Table demand
id | someDatas | deadline1 | deadline2 | deadline3 | deadline4 | deadline5
---+-----------+-----------+-----------+-----------+-----------+-----------
| | | | | |
Table status
id | name
---+-----
|
Table events
id | id_demand | someOthersDatas | id_status
---+-----------+-----------------+----------
| | |
I wrote a query to get the demands corresponding of a list of status :
SELECT dem.*, st.`name` as 'statusName'
FROM `status` st
INNER JOIN `events` eve
ON eve.id_status = st.id
INNER JOIN `demand` dem
ON eve.id_demand = dem.id
WHERE st.`name` IN ('A', 'B', 'C', 'D')
AND eve.id IN
(
SELECT MAX(even.id) ev
FROM `demand` de
INNER JOIN `events` even
ON even.id_demand = de.id
GROUP BY de.id
);
This query works perfectly and I can get the desired informations for my treatment, I have the id of the demands, its deadlines and the name of the current status.
I don't mind storing this result in a temporary table such as :
DROP TEMPORARY TABLE IF EXISTS pendingDemands;
CREATE TEMPORARY TABLE IF NOT EXISTS pendingDemands
SELECT /* the query shown above */
To make sure the day I want to add to the deadline is valid (= not a day off) I wrote a function that calculate the next valid day :
DELIMITER //
DROP FUNCTION IF EXISTS `get_next_valid_date`;
CREATE FUNCTION `get_next_valid_date`(MyDate DATETIME) RETURNS DATETIME
BEGIN
REPEAT
SET MyDate = (DATE_ADD(MyDate, INTERVAL 1 DAY));
SET #someCondition = (select isDayOff(MyDate));
UNTIL (#someCondition = 0) END REPEAT;
RETURN MyDate;
END//
This function works perfectly and I get the expected results, and isDayOff() don't need to be detailed.
My problem is that I don't know how to use them (the temporary table pendingDemands and the function get_next_valid_date) together to update the table demand, I'm not skilled enough in SQL to build such pretty UPDATE query.
Any direction I could take?
I finally found a work around based on this answer
I created a stored procedure in which I'm using a cursor storing the query I was using to feed the pendingDemands temporary table.
Then, I looped over that cursor and used a CASE WHEN statement to determine the values to modify :
DELIMITER $$
DROP PROCEDURE IF EXISTS `freezePendingDeadlines` $$
CREATE PROCEDURE `freezePendingDeadlines`()
BEGIN
-- from http://stackoverflow.com/questions/35858541/call-a-stored-procedure-from-the-declare-statement-when-using-cursors-in-mysql
-- declare the program variables where we'll hold the values we're sending into the procedure;
-- declare as many of them as there are input arguments to the second procedure,
-- with appropriate data types.
DECLARE p_id INT DEFAULT 0;
DECLARE pT2P DATETIME DEFAULT NULL;
DECLARE pT3P DATETIME DEFAULT NULL;
DECLARE pT4P DATETIME DEFAULT NULL;
DECLARE pT5P DATETIME DEFAULT NULL;
DECLARE pstatusName VARCHAR(255) DEFAULT NULL;
-- we need a boolean variable to tell us when the cursor is out of data
DECLARE done TINYINT DEFAULT FALSE;
-- declare a cursor to select the desired columns from the desired source table1
-- the input argument (which you might or might not need) is used in this example for row selection
DECLARE demandCursor
CURSOR FOR
SELECT p.id,
p.T2P,
p.T3P,
p.T4P,
p.T5P,
P.statusName
FROM
(
SELECT dem.*, st.`name` as 'statusName'
FROM `status` st
INNER JOIN `events` eve
ON eve.id_status = st.id
INNER JOIN `demand` dem
ON eve.id_demand = dem.id
WHERE st.`name` IN ('A', 'B', 'C', 'D')
AND eve.id IN
(
SELECT MAX(even.id) ev
FROM `demand` de
INNER JOIN `events` even
ON even.id_demand = de.id
GROUP BY de.id
)
) AS p;
-- a cursor that runs out of data throws an exception; we need to catch this.
-- when the NOT FOUND condition fires, "done" -- which defaults to FALSE -- will be set to true,
-- and since this is a CONTINUE handler, execution continues with the next statement.
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
DROP TEMPORARY TABLE IF EXISTS days_off;
CREATE TEMPORARY TABLE IF NOT EXISTS days_off
(
date_off VARCHAR(5)
);
INSERT INTO days_off VALUES('01-01'),
('05-01'),
('05-08'),
('07-14'),
('08-15'),
('11-01'),
('11-11'),
('12-25');
-- open the cursor
OPEN demandCursor;
my_loop: -- loops have to have an arbitrary label; it's used to leave the loop
LOOP
-- read the values from the next row that is available in the cursor
FETCH demandCursor INTO p_id, pT2P, pT3P, pT4P, pT5P, pstatusName;
IF done THEN -- this will be true when we are out of rows to read, so we go to the statement after END LOOP.
LEAVE my_loop;
ELSE
CASE pstatusName
WHEN 'A' THEN
SET pT2P=get_next_valid_date(pT2P);
SET pT3P=get_next_valid_date(pT3P);
SET pT4P=get_next_valid_date(pT4P);
SET pT5P=get_next_valid_date(pT5P);
WHEN 'B' THEN
SET pT3P=get_next_valid_date(pT3P);
SET pT4P=get_next_valid_date(pT4P);
SET pT5P=get_next_valid_date(pT5P);
WHEN 'C' THEN
SET pT3P=get_next_valid_date(pT3P);
SET pT4P=get_next_valid_date(pT4P);
SET pT5P=get_next_valid_date(pT5P);
WHEN 'D' THEN
SET pT4P=get_next_valid_date(pT4P);
SET pT5P=get_next_valid_date(pT5P);
END CASE;
UPDATE `demand`
SET T2P=pT2P,
T3P=pT3P,
T4P=pT4P,
T5P=pT5P
WHERE id=p_id;
END IF;
END LOOP;
CLOSE demandCursor;
DROP TEMPORARY TABLE IF EXISTS days_off;
END$$
I created the following stored procedure:
CREATE DEFINER=`root`#`localhost` PROCEDURE `add_summit`(IN `assoc_code` CHAR(5), IN `assoc_name` CHAR(50), IN `reg_code` CHAR(2), IN `reg_name` CHAR(100), IN `code` CHAR(20), IN `name` CHAR(100), IN `sota_id` CHAR(5), IN `altitude_m` SMALLINT(5), IN `altitude_ft` SMALLINT(5), IN `longitude` DECIMAL(10,4), IN `latitude` DECIMAL(10,4), IN `points` TINYINT(3), IN `bonus_points` TINYINT(3), IN `valid_from` DATE, IN `valid_to` DATE)
BEGIN
declare assoc_id SMALLINT(5);
declare region_id SMALLINT(5);
declare summit_id MEDIUMINT(8);
-- ASSOCIATION check if an association with the given code and name already exists
SELECT id INTO assoc_id FROM association WHERE code = assoc_code LIMIT 1;
IF (assoc_id IS NULL) THEN
INSERT INTO association(code, name) VALUES (assoc_code, assoc_name);
set assoc_id = (select last_insert_id());
END IF;
-- REGION check if a region with the given code and name already exists
SET region_id = (SELECT id FROM region WHERE code = reg_code AND name = reg_name AND association_id = assoc_id);
IF (region_id IS NULL) THEN
INSERT INTO region(association_id, code, name) VALUES (assoc_id, reg_code, reg_name);
set region_id = (select last_insert_id());
END IF;
-- SUMMIT check if a summit with given parameters already exists
SET summit_id = (SELECT id FROM summit WHERE association_id = assoc_id AND region_id = region_id);
IF (summit_id IS NULL) THEN
INSERT INTO summit(code, name, sota_id, association_id, region_id, altitude_m, altitude_ft, longitude,
latitude, points, bonus_points, valid_from, valid_to)
VALUES (code, name, sota_id, assoc_id, region_id, altitude_m, altitude_ft, longitude, latitude,
points, bonus_points, valid_from, valid_to);
END IF;
END$$
basically, it should check if a record exists in some tables and, if it doesn't, it should insert it and use the inserted id (auto increment).
The problem is that even if the record exists (for instance in the association table), assoc_id keeps returning null and that leads to record duplication.
I'm new to stored procedures so I may be doing some stupid errors. I've been trying to debug this SP for hours but I cannot find the problem.
A newbie mistake.
I forgot to specify the table name in the field comparison and that leads to some conflicts with param names (for example the param name).
A good idea is to specify some kind of prefix for parameters (like p_) and always specify the name of the table in the SP.
I have a table users
in it there are columns
userPoints and userStatus
each user begins as a newbie.
if the userPoints reach 100
user status should change to something else
there is another table userStatuses
statusId statusName minPoints maxPoints
can I create a trigger that will fire off when userPoints reach 100 their userStatus changes, according to the userStatuses table?
table statuses
id statusName minimumPoints maximumPoints
1 lvl1 0 100
2 lvl2 101 1000
3 lvl3 1001 5000
4 lvl4 5001 20000
5 lvl5 20000 100000
Try this:
delimiter //
CREATE TRIGGER changeUserStatus BEFORE UPDATE ON users
FOR EACH ROW
BEGIN
IF NEW.userPoints > 100 THEN
SET NEW.userStatus = 'lvl2';
END IF;
END;//
delimiter ;
delimiter allows the user of semicolons within in the trigger definition.
Of course, assumes that you hard code it. If you instead want to reference a table, I would use a different approach.
EDIT: If you want to reference your table statuses (which you provided in your edited question), then your approach depends on how many records you are updating.
If you are updating one at a time,
delimiter //
CREATE TRIGGER changeUserStatus BEFORE UPDATE ON users
FOR EACH ROW
BEGIN
SET #status = (
SELECT statusName
FROM statuses
WHERE NEW.userPoints BETWEEN s.minimumPoints AND s.maximumPoints
);
IF #status <> NEW.userStatus:
SET NEW.userStatus = #status;
END IF;
END;//
delimiter ;
However, if you are updating many records at a time, it is likely more performant to run one query at the end. Unfortunately, MySQL only allows triggers that perform one operation per row.
So, I would create a stored procedure
CREATE PROCEDURE refreshUserStatuses
UPDATE users u
JOIN statues s ON u.userPoints BETWEEN s.minimumPoints AND s.maximumPoints
SET u.userStatus = s.statusName;
You will have to run CALL refreshUserStatuses; after updating users.
You can do:
CREATE TRIGGER update_status BEFORE UPDATE ON users
FOR EACH ROW
BEGIN
SET #NEWSTATUS = (SELECT statusId
FROM userStatuses
WHERE NEW.userPoints BETWEEN minPoints AND maxPoints);
IF #NEWSTATUS != OLD.userStatus THEN
SET NEW.userStatus = #NEWSTATUS;
END IF;
END;
This will get the status for the user's points and validate if he was already on that level or not. If not, the user will change status.
sqlfiddle demo
This code enables users to change their password:
CREATE TABLE users (
username VARCHAR(20) NOT NULL UNIQUE,
First_name VARCHAR(20),
Last_name VARCHAR(20),
password VARCHAR(20),
PRIMARY KEY (username)
);
CREATE TABLE change_password (
id INT NOT NULL auto_increment,
username VARCHAR(20),
old_password VARCHAR(20),
new_password VARCHAR(20),
comfirm_password VARCHAR(20),
PRIMARY KEY (id)
);
DELIMITER $$
create trigger nn before insert on change_password
for each row
begin
if new.comfirm_password!=new.new_password then
signal sqlstate '42000' set message_text="New password is not the same as comfirm password";
end if ;
if (new.old_password != (select password from users where username=new.username))then
signal sqlstate '42000' set message_text="Incorrect old password";
end if ;
if ((new.old_password = (select password from users where username=new.username)) &&
(new.new_password=new.comfirm_password)) then
update users
set password =new.new_password where username=new.username;
end if;
END $$
DELIMITER ;