Procedure fails but works without variables - mysql

At the moment I'm working with databses and I got a strange error wirth procedures. I have create one which gets three parameters of the type VARCHAR: title, interpret, album. Here is the code:
BEGIN
INSERT IGNORE INTO Interpret (Name) VALUES (interpret);
SET #idInterpret := (SELECT id FROM Interpret WHERE Name = interpret);
INSERT IGNORE INTO Album (Name, Interpret) VALUES (album, #idInterpret);
SET #idAlbum := (SELECT id FROM Album WHERE Name = album AND Interpret = #idInterpret);
INSERT INTO Lied (Name, Album, Interpret) VALUES (title, #idAlbum, #idInterpret);
END
If I start this procedure I get an error which says that the album field can not be null (which is right) but it shouldn't be null because I read the value from the table above. If I call exact the same lines of SQL with real data (not as procedure with varaibles) all works great. Do you have any ideas why this happens?

Avoid naming variables and parameter as columns of your tables.
In your query:
SET #`idAlbum` := (SELECT `id`
FROM `Album`
WHERE `Name` = `album` AND `Interpret` = #`idInterpret`);
Interpret, are you referring to the parameter or column of the table?. We know that is column, MySQL interprets is the parameter.
SQL Fiddle demo
See:
13.6.4.2 Local Variable Scope and Resolution
Name Conflicts within Stored Routines in D.1 Restrictions on Stored Programs
Follow the comment #Bernd-Buffen using local varibales for this case.

To use vars in a Stored Procedure you must DECLARE it and then you
can use them without quotes. I have changed your Query, but not tested.
BEGIN
DECLARE idInterpret DEFAULT ='';
DECLARE idAlbum DEFAULT ='';
INSERT IGNORE INTO Interpret (Name) VALUES (interpret);
SELECT id IN TO idInterpret FROM Interpret WHERE Name = interpret;
INSERT IGNORE INTO Album (Name, Interpret) VALUES (album, idInterpret);
SELECT id INTO idAlbum FROM Album WHERE Name = album AND Interpret = idInterpret;
INSERT INTO Lied (Name, Album, Interpret) VALUES (title, idAlbum, idInterpret);
END

Related

Insert n rows with LAST_INSERT_ID

I have 3 tables called POSTS, HASHTAGS and POSTS_HASHTAGS_RELATION, as below.
CREATE TABLE POSTS(
post_id int unsigned NOT NULL AUTO_INCREMENT,
content varchar(200) NOT NULL,
PRIMARY KEY (post_id)
);
CREATE TABLE HASHTAGS(
hashtag_id int unsigned NOT NULL AUTO_INCREMENT,
hashtag varchar(40) NOT NULL,
PRIMARY KEY (hashtag_id)
);
CREATE TABLE POSTS_HASHTAGS_RELATION(
post_id int unsigned NOT NULL,
hashtag_id int unsigned NOT NULL,
PRIMARY KEY (post_id, hashtag_id)
);
When user posts, they select upto 20 hashtags from those saved in HASHTAGS. I send the hashtag_id(s) of these hashtags from front end as comma separated string to backend where it is converted to list in nodejs.
Firstly, is there a better approach to struture this?
Secondly, how do I insert variable number of rows to POSTS_HASHTAGS_RELATION in a single query?
INSERT INTO POSTS (content) VALUES ('bla bla bla bla');
SET #post_id = LAST_INSERT_ID();
INSERT INTO POSTS_HASHTAGS_RELATION (post_id, hashtag_id) VALUES (#post_id, 19), (#post_id, 41) ...;
// Something like this but the number of values can be between 1 and 20
If this has been answered befored, just guide me to that answer. I am unable to find anything relevant. I assume this is not a very unique problem.
What you show is the way I would do it. You can insert multiple rows using INSERT in the manner you show, by writing multiple row constructors after the VALUES keyword. If you do, you must include a value for all the columns named in your INSERT statement in every row constructor. Therefore you must reference the #post_id variable in each row constructor.
If you really don't like to write #post_id more than once, you could do something like this:
INSERT INTO POSTS_HASHTAGS_RELATION (post_id, hashtag_id)
SELECT #post_id, hashtag_id FROM (
SELECT 19 AS hashtag_id UNION SELECT 41 UNION SELECT 42 UNION ...
) AS t;
But that seems less clear and readable than the way you were doing it.
Re your comment:
I'm not a node.js programmer, but I've used the technique in other languages to build an SQL statement with a number of row constructors based on the input list. Proper query parameters can only be used in place of scalar values, not lists or expressions or identifiers or SQL keywords, etc. But I understand node.js does some extra string-substitution magic, so they're not really doing query parameters.
Suppose you had just done your INSERT into POSTS, you could capture the last insert id:
var postId = result.insertId;
Then create a partial INSERT statement for your next insert:
insert = 'INSERT INTO POSTS_HASHTAGS_RELATION (post_id, hashtag_id) VALUES';
You will need an array for the row constructors and an array for the parameters:
let rowConstructors = [];
let parameters = [];
hashtags.forEach(function (hashtag) {
rowConstructors.push('(?, ?)');
parameters.concat(postId, hashtag);
});
Now you have an array of row constructors, which toString() will turn into a comma-separated string. And you have an array of values to pass as parameters.
connection.query(insert + rowConstructors.toString(),
parameters, function (error, results, fields) {
if (error) throw error;
// ...
});
I guess the .toString() is optional, because the array should be coerced to a string automatically by concatenating it to the insert string.
Again, I'm not a node.js programmer, so forgive me if there are errors or style problems. But that should give you the idea of this technique.
Saving the data into the tables in SP format:
CREATE PROCEDURE save_post_and_tags (post_content VARCHAR(200), hashtag_ids TEXT)
BEGIN
DECLARE post_id INT UNSIGNED;
DECLARE tag_id INT UNSIGNED;
INSERT INTO posts VALUES (DEFAULT, post_content);
SET post_id := LAST_INSERT_ID();
SET hashtag_ids := concat(hashtag_ids, ',');
REPEAT
SET tag_id := 0 + SUBSTRING_INDEX(hashtag_ids, ',', 1);
SET hashtag_ids := TRIM(SUBSTRING(hashtag_ids FROM 1 + LOCATE(',', hashtag_ids)));
INSERT INTO posts_hashtags_relation VALUES (post_id, tag_id);
UNTIL hashtag_ids = '' END REPEAT;
END
https://dbfiddle.uk/?rdbms=mysql_8.0&fiddle=a9c622ea7ad2dd48dba18312c7a33487 (sloppy CSV is used on purpose).

Create table and insert rows if doesn't exist, else just insert

I am able to create table, shred JSON and add data if it does not exist in SQL Server:
DECLARE #json nvarchar(max);
SET #json = N'[{"IplayerName": "Pilipiliz",
"Sname": "kikombe",
"WeightLBs":"60.236"
}]'
IF NOT EXISTS (SELECT * FROM sys.objects WHERE OBJECT_ID = object_id('Iplayerds'))
BEGIN
SELECT
[IplayerName],
[Sname],
[WeightLBs]
INTO
Iplayerds
FROM
OPENJSON(#json)
WITH (IplayerName NVARCHAR(200),
Sname NVARCHAR(20),
WeightLBs DECIMAL(10,4)
)
END
ELSE
PRINT 'exists'
However, when I try to replace the print statement with insert rows code shown below, it fails
INSERT INTO Iplayerds (IplayerName, Sname, WeightLBs)
VALUES ([IplayerName], [Sname], [WeightLBs]
FROM OPENJSON(#json))
What am I doing wrong?
The INSERT command comes in two flavors:
(1) either you have all your values available, as literals or SQL Server variables - in that case, you can use the INSERT .. VALUES() approach:
INSERT INTO dbo.Iplayerds (IplayerName, Sname, WeightLBs)
VALUES (#IplayerName, #Sname, #WeightLBs)
Note: I would recommend to always explicitly specify the list of column to insert data into - that way, you won't have any nasty surprises if suddenly your table has an extra column, or if your tables has an IDENTITY or computed column. Yes - it's a tiny bit more work - once - but then you have your INSERT statement as solid as it can be and you won't have to constantly fiddle around with it if your table changes.
(2) if you don't have all your values as literals and/or variables, but instead you want to rely on another table, multiple tables, or views, to provide the values, then you can use the INSERT ... SELECT ... approach:
INSERT INTO dbo.Iplayerds (IplayerName, Sname, WeightLBs)
SELECT
[IplayerName], [Sname], [WeightLBs]
FROM
OPENJSON(#json) WITH (IplayerName NVARCHAR(200),
Sname NVARCHAR(20),
WeightLBs DECIMAL(10,4)
)
Here, you must define exactly as many items in the SELECT as your INSERT expects - and those can be columns from the table(s) (or view(s)), or those can be literals or variables. Again: explicitly provide the list of columns to insert into - see above.
You can use one or the other - but you cannot mix the two - you cannot use VALUES(...) and then have a SELECT query in the middle of your list of values - pick one of the two - stick with it.

"INSERT IF NOT EXISTS", ELSE UPDATE certain columns in MySQL

I'm using MySQL Workbench (6.3) and I'm trying to create a stored procedure with a specific "INSERT IF NOT EXSISTS" but I think I don't do it well. By the way, I did read this topic http://bogdan.org.ua/2007/10/18/mysql-insert-if-not-exists-syntax.html and tried both examples, it didn't work. Here is the statement :
CREATE DEFINER=`test`#`localhost` PROCEDURE `onPlayerJoin`(
IN uuid CHAR(36),
IN nickname CHAR(16),
IN firstConnection TIMESTAMP,
IN lastConnection TIMESTAMP
)
BEGIN
INSERT IGNORE INTO `test`.`player` (`playerUuid`, `playerNickname`, `playerFirstConnection`, `playerLastConnection`)
VALUES (uuid, nickname, firstConnection, lastConnection);
UPDATE `test`.`player` SET
`playerNickname` = nickname,
`playerLastConnection` = lastConnection
WHERE `playerUuid` = uuid;
END
IF the PK isn't found, INSERT it. ELSE, UPDATE certain columns. (that I can specified) However, it seems that it updates every column, which I would like to restrict to certain columns. Here is my procedure : http://pastebin.com/NfcdU9Rb !
Optional question : is it injection-safe ?
Use INSERT ... ON DUPLICATE KEY UPDATE Syntax
INSERT INTO table (a,b,c) VALUES (1,2,3)
ON DUPLICATE KEY UPDATE c=c+1;
Kindly refer the link for details:
http://dev.mysql.com/doc/refman/5.6/en/insert-on-duplicate.html

Recursive MySQL Query with relational innoDB

I've got a table structure like (simplified):
content
- id_content
- my_string1
- ...
content_has_content
- id_content
- id_subcontent
topic_has_content
- id_topic
- id_content
Any topic can have multiple 'content', any 'content' can have multiple 'subcontent' (instance of content).
With a given id_topic I'd like to recieve a list of all my_string1 from the linked contents, subcontents, subcontents-of-subcontents, and so on.
I understand "WITH" is not working for mysql but cannot find a nice recursive alternative.
Thanks
daniel
There is no recursion in MySQL and also the result you would get would be flat (no structure). The best way is still a while loop in PHP, Java or whatever programming language you use.
The query could look like this:
SELECT C.*, CHC.ID_SUBCONTENT
FROM CONTENT C
LEFT OUTER JOIN CONTENT_HAS_CONTENT CHC ON CHC.ID_CONTENT = C.ID_CONTENT
WHERE C.ID = ?
... // you get the idea
and in PHP you could repeat the query with the next sub_content_id, until ID_SUBCONTENT is null
Solution
PosgreSQL, Oracle, MS-SQL, ... have WITH RECURSIVE to handle such data structures. It internally uses a while loop to get all the parent ids for the current row (we need the child's here instead)
This can achieved in MySQL too, you can create a stored procedure and reproduce the same behavior
Assumed/DDL used
content_has_content has entries as
content_1
content_2
content_3
content_4
I pass the id_content as 2 and get the output as
content_2
content_3
content_4
All of the above are descendants of content_2
Fire call content_details(2); to get all the child rows for the passed id_content
SQL
###### Stored Routine
DELIMITER //
drop procedure IF EXISTS `content_details`;
CREATE DEFINER=`root`#`localhost` PROCEDURE `content_details`(`_idcontent` INT)
LANGUAGE SQL
DETERMINISTIC
CONTAINS SQL
SQL SECURITY DEFINER
COMMENT ''
BEGIN
DECLARE temp_content_ids varchar(200);
DECLARE idcontent, idcontent2 integer;
SET temp_content_ids= _idcontent;
SET idcontent = _idcontent;
SET idcontent2 = _idcontent;
WHILE idcontent2 IS NOT NULL DO
SET idcontent = NULL;
SELECT id_subcontent, CONCAT(id_subcontent,',',temp_content_ids) INTO idcontent, temp_content_ids FROM content_has_content WHERE id_content = idcontent2;
SET idcontent2 = idcontent;
END WHILE;
SELECT my_string1 FROM content WHERE FIND_IN_SET( id, temp_content_ids );
END//
What I basically do is run a while loop until I have the last child_id, store these ids in a comma separated string format and then fire a query to get all the rows which have a id present in the var I just created
Note: There are chances you could be having invalid values within your tables, such as row having a id_subcontent, which points to its own id_content, which could cause a never ending loop within the procedure, to avoid such situations, you can use a counter and limit the nesting to say around 50 (or any value as per your requirement) and raise a exception if that limit is surpassed
Some data to play with..
###### DLL Statements
CREATE TABLE content
( id_content int,
my_string1 varchar(200));
CREATE TABLE content_has_content
( id_content int,
id_subcontent int);
CREATE TABLE topic_has_content
( id_topic int,
id_content int);
INSERT INTO content VALUES (1, 'content_1'), (2, 'content_2'), (3, 'content_3'), (4, 'content_4');
INSERT INTO content_has_content VALUES (1, 2), (2, 3), (3, 4);
INSERT INTO topic_has_content VALUES (1, 1);
Hope this helps..

Throwing Custom Errors in MySQL store procedures

I'm playing around with MySQL stored procedures and I need a little help wrapping my head around some things. Below I'm attempting to;
1) Check if the student_id exist in the database and if it does then display "alumni already exist"
2) Check to see if the department and degree parameter entered don't exist and if it doesn't, then display "_ does not exist" (side note : these two columns are foreign keys)
Right now, my IF statement doesn't work and throws arbitrary errors. (ex. student_id doesn't exist in table but the error "Alumni Exist Already" is thrown, this is one of many)
I'd like to know what I'm doing wrong. Also, If the way I'm approaching this makes sense and if it doesn't, what's a more pragmatic way of going about this?
Thanks
DELIMITER //
DROP PROCEDURE IF EXISTS sp_add_alumni//
CREATE PROCEDURE sp_add_alumni (
IN student_id INT(20),
IN first_name VARCHAR(255),
IN last_name VARCHAR(255),
IN street VARCHAR(255),
IN city VARCHAR(255),
IN state VARCHAR(2),
IN zip_code VARCHAR(15),
IN email VARCHAR(255),
IN telephone VARCHAR(22),
IN degree VARCHAR(255),
IN department VARCHAR(255)
)
BEGIN
DECLARE studentID INT(20);
DECLARE departmentVAL VARCHAR(255);
DECLARE degreeVal VARCHAR(255);
DECLARE EXIT HANDLER FOR SQLWARNING
BEGIN
ROLLBACK;
SELECT 'ALUMNI INSERT HAS FAILED';
END;
SET studentID = student_id;
SET departmentVal = department;
SET degreeVal = degree;
IF EXISTS (SELECT 1 FROM alumni WHERE student_id = studentID ) THEN
SELECT 'ALUMNI ALREADY EXISTS';
ELSEIF NOT EXISTS (SELECT 1 FROM valid_departments WHERE UCASE(department) = UCASE(departmentVal)) THEN
SELECT 'DEPARTMENT DOES NOT EXISTS';
ELSEIF NOT EXISTS (SELECT 1 FROM valid_degrees WHERE UCASE(degree) = UCASE(degreevVal)) THEN
SELECT 'DEGREE DOES NOT EXISTS';
ELSE
SELECT 'ALUMNI ADDED';
END IF;
START TRANSACTION;
INSERT INTO alumni (student_id, pwd ,first_name, last_name, street, city, state, zip_code, email, telephone, degree, department, role_id, donation_total) VALUES (student_id, NULL ,first_name, last_name, street, city, state, zip_code, email, telephone, degree, department, 1, 0.00);
COMMIT;
END//
I'd like to know what I'm doing wrong.
As documented under Restrictions on Stored Programs:
Name Conflicts within Stored Routines
The same identifier might be used for a routine parameter, a local variable, and a table column. Also, the same local variable name can be used in nested blocks. For example:
CREATE PROCEDURE p (i INT)
BEGIN
DECLARE i INT DEFAULT 0;
SELECT i FROM t;
BEGIN
DECLARE i INT DEFAULT 1;
SELECT i FROM t;
END;
END;
In such cases, the identifier is ambiguous and the following precedence rules apply:
A local variable takes precedence over a routine parameter or table column.
A routine parameter takes precedence over a table column.
A local variable in an inner block takes precedence over a local variable in an outer block.
The behavior that variables take precedence over table columns is nonstandard.
In your case student_id is a routine parameter and studentID is a local variable; therefore (given the precedence rules above) the filter criterion WHERE student_id = studentID is comparing those two things with eachother and at no time is inspecting a table column.
Since the local variable was set to the value of the routine parameter, this filter always evaluates to true.
You could avoid this either by using different names for your parameters/variables, or else by qualifying your column reference with a table prefix:
WHERE alumni.student_id = studentID
Also, If the way I'm approaching this makes sense and if it doesn't, what's a more pragmatic way of going about this?
Define suitable UNIQUE and foreign key constraints, then attempts to insert invalid data will fail without you explicitly having to check anything:
ALTER TABLE alumni
ADD UNIQUE KEY (student_id), -- is this not already a PRIMARY KEY ?
ADD FOREIGN KEY (department) REFERENCES valid_departments (department),
ADD FOREIGN KEY (degree ) REFERENCES valid_degrees (degree )
;
To make the foreign keys use a case-insensitive lookup, ensure that the respective columns use a case insensitive collation.
Note the restrictions to which foreign keys are subject in the article linked above.