Mysql do not update if row does not exist - mysql

I've created a web app that lets you send money to other people (like paypal) for an University project.
For the send money webpage there's a form where an user can choose the money receiver's ID and the amount to send.
The system works with these queries:
SET #moneytosend= ? ; //amount to send
START TRANSACTION;
UPDATE users SET balance= balance- #moneytosend WHERE id = ?; //sender's ID
UPDATE users SET balance= balance+ #moneytosend WHERE id = ?; //receiver's ID
COMMIT WORK;
The problem is when the user inserts a non existing ID and the query is still executed, effectively removing money from the sender.
I'm sorry if my english is not perfect, and sorry again if there are any formatting errors.

One method is to check that the users exist in the update queries:
UPDATE users JOIN
(SELECT ? as sender_id, ? as receiver_id
) uu
ON u.id IN (uu.sender_id, uu.receiver_id)
SET balance = balance + (CASE WHEN u.id = uu.receiver_id THEN #moneytosend ELSE - #moneytosend)
WHERE EXISTS (SELECT 1 FROM users u2 WHERE u2.id = uu.sender_id) AND
EXISTS (SELECT 1 FROM users u2 WHERE u2.id = uu.receiver_id);
This logic combines the queries into a single query and still allows you to input only two parameters.

The problem is caused in the 1st update statement and it can be solved by adding EXISTS as a condition:
update users
set balance = balance - #moneytosend
where
balance >= #moneytosend
and
id = ? <-- sender id
and
exists (
select 1 from (select * from users where id = ? <-- receiver id
) t);
See the demo
As suggested by Raymond Nijland, do an extra check so that balance does not get a negative value.

Related

How do I update one column value to contain all of the value in two columns of a table in MySQL?

I am a college student currently studying SQL attack and prevention. There is an exercise where we need to
Update your first_name to be the email and password of all users who is
an admin (assume that there is a field in the users table called
is_admin where it's 0 if the user is not an admin, or 1 if the user is
an admin).
This way, when you log out and log back in, instead of saying Welcome
[your first_name], it would say Welcome [whatever was stored in
first_name field].
Lets assume that there is 6 row in my users table and that my id is 6
I tried to use group concat for email and password
SELECT group_concat(email, " ", password)
AS account_information FROM users
WHERE is_admin = 1
So far it works, it returned 1 row with all of the email and password of users who are an admin and I thought that this is the code I should subquery to be set for my first_name. And now I subqueried it to update my first_name with this code.
UPDATE users
SET first_name = (SELECT group_concat(email, " ", password) AS account_information
FROM users
WHERE is_admin = 1)
WHERE id = 6
I got an error 1093: saying that I can't specify target table 'users' for UPDATE in FROM clause
Can someone help me with this exercise?
Use a CROSS join of the table to a query that returns the concatenated values:
UPDATE users u
CROSS JOIN (SELECT GROUP_CONCAT(email, ' ', password) new_name FROM users WHERE is_admin = 1) t
SET u.first_name = t.new_name
WHERE u.id = 6;

UPDATE using SELECT as WHERE

I want to run an Update where the WHERE statement consists of 2 SELECTS, is this at all close to how you do that?
UPDATE Requests SET Response=1 WHERE
sender=SELECT userID FROM Users WHERE Username=?) and
Reciever = SELECT userID FROM Users WHERE Username=?
Thank!
You're missing parentheses, that's all.
UPDATE Requests SET Response=1 WHERE
sender = (SELECT userID FROM Users WHERE Username=?) and
receiver = (SELECT userID FROM Users WHERE Username=?);

How to use data in same table on UPDATE query?

update accounts set password=(select password from accounts where name='joongsu')
where id=(select accountid from characters where name='Nobless')
it doesn't work with error message "You can't specify target table 'accounts' for update in FROM clause"
Why doesn't it work? select queries in above only return 1 row.
Perhaps you should try this one:
UPDATE accounts
SET accounts.password =
(
SELECT something.password
FROM (SELECT * FROM accounts) AS something
WHERE something.name='joongsu'
)
WHERE accounts.id=(SELECT accountid FROM characters WHERE name='Nobless');
It's a hack, but I tested it and it works on my test data. For some reason MySQL doesn't allow using the same table in inner queries as the one being updated.
UPDATE
accounts AS account_to_be_updated
JOIN
characters
ON characters.accountid = account_to_be_updated.id
AND characters.name = 'Nobless'
CROSS JOIN
( SELECT password
FROM accounts
WHERE name = 'joongsu'
) AS existing_account
SET
account_to_be_updated.password = existing_account.password ;
Is this what you looking out for?
;with CTE as
(
select password from accounts where name='joongsu' limit 1
)
update accounts set password= CTE.password
where id in
(select accountid from characters where name='Nobless')

Conditional sql WHERE clause based on column values

I have a table that stores users. Every user has an ID, a Name and an Access Level. The three possible Access Levels are Administrator, Manager and Simple User.
What I want is to conditionally select from this table based on the Access Level value. I demonstrate the logic bellow:
If user is Administrator, then select all users (Administrators, Managers, Simple Users)
Else if user is Manager select all Managers and all Simple Users
Else if user is Simple User select only himself
Is that possible?
Providing Access Level is an integer that increases with the actual access level:
Administrator = 3
Manager = 2
User = 1
Then
SELECT * FROM USERS
WHERE ACCESS_LEVEL <= (SELECT ACCESS_LEVEL FROM USERS WHERE ID = #ID)
And just switch the less than sign to greater than if it is opposite.
Applies to SQL-server
EDIT: To get what you want you can use something like this:
SELECT id,name FROM users where ID = #id
UNION DISTINCT
SELECT id, name from users where access_level <=
(select case when access_level = 1 then 0 else access_level end
from users where id = #id)
With a query like this you will select all users at your current access_level or lower.
Only exception if your access_level is 1 (e.g. normal user), then only your own user is selected.
In Oracle the concept is known as Row Level Security. See articles such as this or this to get you started.
You can use following code
If(access_level=='administrator'){ str query = 'select * from users';}else if(access_level=='manager'){ str query = 'select * from users where access_level!="administrator"'; }else{ str query = 'select * from users where access_level="users"'; }
Might be a little overkill, but the following stored procedure can be used to achieve what you want:
DROP procedure IF EXISTS `listAccessibleUsers`;
DELIMITER $$
CREATE PROCEDURE `listAccessibleUsers`(IN user_id INT)
BEGIN
DECLARE u_access_level INT;
-- Select the user access level
SELECT access_level
FROM users
WHERE users.id = user_id
INTO u_access_level;
-- Result for simple users
IF u_access_level = 1 THEN
SELECT id, name, access_level
FROM users
WHERE users.id = user_id;
-- Result for admistrators and managers
ELSE
SELECT id, name, access_level
FROM users
WHERE access_level <= u_access_level;
END IF;
END$$
DELIMITER ;
Example calling code:
CALL listAccessibleUsers(200);

mysql query all records with no timestamp before x date

I have table that records logins for users. I'm having trouble finding a query that will efficiently pull all unique users who have not had a login timestamp before a date of my choosing. I can easily find this information if I query per-user.
Fields are essentially:
id, username, login, logout
I need a query that will find all usernames that have no logout timestamp in 2011-11. The fact that there are multiple join events for a single easier is what's confusing me.
Exclude all users that had at least one logout at the date in question with a negated semi join (EXISTS) like this:
SELECT u.username
FROM users u
WHERE NOT EXISTS (
SELECT *
FROM log
WHERE log.username = u.username
AND date(logout) = '2011-11-11')
This will not multiply rows from users. If 2011-11 is supposed to signify the month of November 2011:
...
AND logout >= '2011-11-01 0:0'
AND logout < '2011-12-01 0:0'
Additional answer to question in comments
Flag the first login of users in the log
ALTER TABLE log ADD COLUMN first_log boolean; -- boolean flag
UPDATE log
,(SELECT username, min(login) AS min_log
FROM log
GROUP BY 1) x
SET log.first_log = TRUE
WHERE (log.username, log.login) = (x.username, x.min_log);
You can do that, but there are many more rows in table log than in table users. I would advise instead:
ALTER TABLE users ADD COLUMN first_log datetime;
UPDATE users u
,(SELECT username, min(login) AS min_log; -- datetime col
FROM log
GROUP BY 1) x
SET u.first_log = x.min_log
WHERE u.username = x.username