I'm trying to migrate an SQL Server 2012 trigger to MySQL. The trigger helps to create a hierarchical path for a tree of parent-child nodes which looks something like ".level1.level2.level3" and so on. However, I'm having some issues getting it to work. Here is the SQL Sever trigger creation:
CREATE TRIGGER [dbo].[trigger_TiersPath] ON [dbo].[Tiers] AFTER INSERT, UPDATE
AS
BEGIN
-- This trigger will update the hierarchy diagram for the inserted/updated node and all descendants.
IF (UPDATE(ParentID)) -- Ignore updates to other data fields
BEGIN
-- First, all updated nodes will start with the same string: the new hierarchy diagram of the
-- updated/inserted node. Find this first, then use it as the base for all updated values.
DECLARE #newDiagramStart VARCHAR(MAX)
-- Just use a dot if this is the top of the hierarchy. Otherwise, obtain the hierarchy diagram of the new parent.
SELECT #newDiagramStart = ISNULL(Tiers.TierPath, '.') FROM inserted LEFT JOIN Tiers ON Tiers.TierID = inserted.ParentID
-- Append the inserted/updated node ID.
SELECT #newDiagramStart = #newDiagramStart + CAST(TierID AS VARCHAR(MAX)) + '.' FROM inserted
-- Now update the hierarchy diagram for the inserted/updated node and any descendants.
UPDATE NodesDescendants
-- Find the existing hierarchy diagram after the node ID of the inserted/updated node, and append it.
SET TierPath = #newDiagramStart + ISNULL(RIGHT(NodesDescendants.TierPath, LEN(NodesDescendants.TierPath) - LEN(inserted.TierPath)),'')
FROM inserted
INNER JOIN Tiers AS NodesDescendants
ON NodesDescendants.TierID = inserted.TierID -- Update the inserted/updated node.
OR NodesDescendants.TierPath LIKE inserted.TierPath + '%' -- Update all descendants.
END
END
;
So far, this is what I have for the Insert portion of the equivalent MySQL trigger:
DELIMITER //
CREATE TRIGGER trigger_TiersPath
BEFORE INSERT
ON Tiers FOR EACH ROW
BEGIN
DECLARE newDiagramStart LONGTEXT;
CREATE TEMPORARY TABLE IF NOT EXISTS InsTiers
(
TierID VARCHAR(50),
ParentID VARCHAR(50),
OrgID VARCHAR(50),
TierPath VARCHAR(2048),
Active CHAR(1),
CreatedON DATETIME,
CreatedBY VARCHAR(50),
BatchID INT
);
DELETE FROM InsTiers;
INSERT INTO InsTiers VALUES(NEW.TierID, NEW.ParentID, NEW.OrgID, NEW.TierPath, NEW.Active, NEW.CreatedON, NEW.CreatedBY, NEW.BatchID);
BEGIN
-- This trigger will update the hierarchy diagram for the inserted/updated node and all descendants.
IF (1 = 1) then -- Ignore updates to other data fields
-- First, all updated nodes will start with the same string: the new hierarchy diagram of the
-- updated/inserted node. Find this first, then use it as the base for all updated values.
-- Just use a dot if this is the top of the hierarchy. Otherwise, obtain the hierarchy diagram of the new parent.
select IFNULL(Tiers.TierPath,'.') INTO newDiagramStart FROM InsTiers inserted LEFT JOIN Tiers ON Tiers.TierID = inserted.ParentID;
-- Append the inserted/updated node ID.
-- Find the existing hierarchy diagram after the node ID of the inserted/updated node, and append it.
-- Update the inserted/updated node.
SET newDiagramStart = CONCAT_WS('', newDiagramStart, CAST(NEW.TierID AS CHAR), '.');
-- Now update the hierarchy diagram for the inserted/updated node and any descendants.
NEW.TierPath = CONCAT_WS('',newDiagramStart,IFNULL(RIGHT(NULL, CHAR_LENGTH(NULL) - CHAR_LENGTH(NEW.TierPath)),''));
end if;
END;
END;
//
However, this fails due to some syntax error related to both of the CONCAT_WS lines. I just can't see it. I'm open to suggestions to fix this; or even, finding a better approach to achieve the same ends. Unfortunately, I'm stuck with keeping the whole ".level1.level2.level3" type of concatenated path. In my case those "levels" are actually IDs.
Thanks ahead of time.
Related
CREATE PROCEDURE Sp_IU_Group(
GID int,
GroupName nvarchar(200),
UserID int,
Status int
)
BEGIN
IF GID=0 THEN
Insert into tblGroup (GroupName,UserID,Status)
values (GroupName,UserID,Status);
else
update tblGroup set GroupName=GroupName,UserID=UserID,Status=Status WHERE GID=GID;
END IF;
END
This query:
update tblGroup set GroupName=GroupName,UserID=UserID,Status=Status WHERE GID=GID
Will update every record in the table... to itself. This matches every record, because this is always true:
WHERE GID=GID
And this updates a value to itself:
GroupName=GroupName
The problem is that you're using the same names for multiple things. Give things different names. Something as simple as this:
CREATE PROCEDURE Sp_IU_Group(
GIDNew int,
GroupNameNew nvarchar(200),
UserIDNew int,
StatusNew int
)
(Or use any other standard you want to distinguish the variables from the database objects, such as prepending them with a special character like an #.)
Then the query can tell the difference:
update tblGroup set GroupName=GroupNameNew,UserID=UserIDNew,Status=StatusNew WHERE GID=GIDNew
(Modify the rest of the stored procedure for the new variable names accordingly, of course.)
Basically, as a general rule of thumb, never rely on the code to "know what you meant". Always be explicit and unambiguous.
I have a stored procedure, internally I want to call another procedure that returns a record set, how do I get an navigate the record set returned by the stored procedure via the 'CALL' ?
[edit] I've been trying to use a TEMPORARY TABLE as suggested, but having problems:
DROP TEMPORARY TABLE IF EXISTS tbl_HeadOfDepts;
CREATE TEMPORARY TABLE tbl_HeadOfDepts (biDept_id tinyint(4))
INSERT INTO tbl_HeadOfDepts CALL rsHeadOfAnyDepartments(vcCompKey, biWho_id);
I need to use CALL because 'rsHeadOfAnyDepartments' is not a function, but this will not be accepted.
Work in progress, but what I have so far that is not accepted by editor:
BEGIN
#--
# Procedure:
# rsWhoCanIaccess
#
# Parameters:
# vcCompKey, the key corresponding to the company
# biWho_id, the id of the person to check access for
#
# Returns:
# recordset containing all the people this person can access
#--
DECLARE tiSuperUser tinyint(4);
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
GET DIAGNOSTICS CONDITION 1 #sqlstate = RETURNED_SQLSTATE,
#errno = MYSQL_ERRNO, #text = MESSAGE_TEXT;
CALL procLogError(vcCompKey, CONCAT("rsWhoCanIaccess: "
,#errno, " (", #sqlstate, "): ", #text));
END;
#Is this user a super user?
SELECT tiIsSuperUser(vcCompKey, biWho_id) INTO tiSuperUser;
SET tiSuperUser = 0;#Hack for testing
IF (tiSuperUser = 1) THEN
#The user is a superuser, return everyone in the company
SELECT
t1.biPerson_id
FROM
tbl_people t1
INNER JOIN
tbl_companies t2
ON
t1.biCompany_id=t2.biCompany_id
AND
t2.vcKey=vcCompKey;
ELSE
#User is not a superuser, is the user head of any departments?
DROP TEMPORARY TABLE IF EXISTS tbl_HeadOfDepts;
CREATE TEMPORARY TABLE tbl_HeadOfDepts (biDept_id tinyint(4))
INSERT INTO tbl_HeadOfDepts CALL rsHeadOfAnyDepartments(vcCompKey, biWho_id);
SELECT * FROM tbl_HeadOfDepts;
END IF;
END
No, stored procedures can produce result sets, but not consume them directly as output from inner calls to other stored procedures. The best that you can do performance-wise is to populate a non-temporary work table and use the results.
Depending on your software and the reality of multiple callers concurrently, you might need to include a session id concept with an auto_increment (AI) column in some control table. This would ensure that with concurrency, multiple callers are not stomping on each other's rows, thus making it non-viable.
How that session would work, at a high level, is the following. The inner stored proc would be handed an AI value (theSession) from the control table, use it to populate a safely segmented session in the work table, and return as an out parameter to the outer (calling) stored proc. That outer one could then safely use those rows, and clean up at the end (delete from workTable where sessionId=theSession).
Why do I suggest a non-temporary work table? To be clear, the work table would be non-temporary. First of all there is the hassle of getting the if exists drop to work. Most importantly, though, it is about performance. DDL calls for temporary table creation are not inexpensive. You will only believe this when you do performance testing to see what I mean. It may seem trivial, but in trivial operations, those DDL calls for creation could very well account for the lion share of the time necessary for the inner stored proc to complete.
internally I want to call another procedure that returns a record set,
In your inner procedure create a TEMPORARY TABLE and populate that temp table saying insert into your_temp_table select query. then you can use that same temp table in your outer query anywhere.
It can even be a normal table as well and need not be temporary table. Also make sure to DROP the table once your procedure computation done as clean-up.
That's wrong per your comment. You should do it like below (a sample code)
create procedure rsHeadOfAnyDepartments(vcCompKey varchar(10), biWho_id int)
as
begin
DROP TEMPORARY TABLE IF EXISTS tbl_HeadOfDepts;
CREATE TEMPORARY TABLE tbl_HeadOfDepts(col1 int, col2 varchar(10), col3 varchar(30));
INSERT INTO tbl_HeadOfDepts
SELECT col1, col2, col3
FROM tblTest;
end
I don't know how to solve this case need some guidance I have a scenario where I have to update a star column field of root node on adding every child entry so far I have done this, its updating the star column of directly linked nodes not the full path like if A is root and B C & D are children then A star column gets update now if C B or D adds something below them A stars column doesn't gets updated, I have less knowledge about triggers and sql and I am stuck I have searched a lot but didn't find a solution here is the method of sql which is doing all this so far.
DELIMITER ##
DROP PROCEDURE p_prefix_nodes_add_new_paths_after_insert ##
CREATE PROCEDURE cvs.p_prefix_nodes_add_new_paths_after_insert
(
param_node_new_id INT UNSIGNED,
param_node_parent_id INT UNSIGNED
)
BEGIN
INSERT INTO `prefix_nodes_paths` (
`ancestor_id`,
`descendant_id`,
`path_length`
)
SELECT
`ancestor_id`,
`param_node_new_id`,
`path_length` + 1
FROM
`prefix_nodes_paths`
WHERE `descendant_id` = `param_node_parent_id`
UNION
ALL
SELECT
`param_node_new_id`,
`param_node_new_id`,
0 ;
Update prefix_nodes_paths
Set stars=stars+1
where ancestor_id=param_node_parent_id;
END ##
DELIMITER ;
Id's suggest you to use the nested set model technique to store your tree-like data in the SQL database. Your queries would become a lot more handy, faster and simpler when you try to get descendants or ancestors of the specific node.
What I'm trying to achieve is, I want to automate the values of the table between the users and folders table. Since it's a many-to-many relationship I created the user_folders table. Currently the server (nodejs) gets the request with userid, clientfolderid and some an array of bookmarks (which are not important now). It checks if the user already has this folder, by selecting from the user_folders table and if it's not existing it inserts a new row into the folder table. Then it has to send another statement to insert into the user_folders table.
So I have to "manually" keep the users_folder table updated.I guess this is a common problem and wanted to know if there is a pattern or a proven solution? The odd thing is that MySQL automatically handles the deletion of rows with an AFTER DELETE trigger but there is no (at least that I know of) automation with an AFTER INSERT trigger.
As I already said an AFTER INSERT trigger could possibly solve it, but I think it's not possible to pass some extra parameters to the AFTER INSERT trigger. This would be the user_id and the folder_client_id in my case.
I was thinking of a solution that I could create another table called tmp_folder which would look like:
tmp_folder
-- id
-- title
-- changed
-- user_id
-- folder_client_id
Then create an AFTER INSERT trigger on this table which inserts into folders and user_folders and then removes the row from tmp_folder again. Would this be the right way or is there a better one?
I would basically do the same with the bookmarks and user_bookmarks table. The best thing would be if it's even possible to insert a folder then the owner into the user_folders table with user_id and folder_client_id and then multiple other users into user_folders with the user_id and an default folder_client_id of -1 or something which will be updated later.
Meanwhile thanks for reading and I hope you can help me :)
PS: Is there a name for the table between 2 other tables in an m-2-m relationship?
I don't see an easy way to do this via triggers, but a stored procedure may suit you:
DELIMITER //
CREATE PROCEDURE
add_user_folder(
IN u_user_id BIGINT UNSIGNED,
IN u_folder_client_id BIGINT UNSIGNED,
IN v_title VARCHAR(255)
)
BEGIN
DECLARE u_found INT UNSIGNED DEFAULT 0;
SELECT
1 INTO u_found
FROM
user_folders
WHERE
user_id = u_user_id AND
folder_client_id = u_folder_client_id;
IF IFNULL(u_found, 0) = 0 THEN
START TRANSACTION;
INSERT INTO
folders
SET
title = v_title,
changed = UNIX_TIMESTAMP();
INSERT INTO
user_folders
SET
user_id = u_user_id,
folder_id = LAST_INSERT_ID(),
folder_client_id = u_folder_client_id;
COMMIT;
END IF;
END;
//
I have a trigger in which I want to have a variable that holds an INT I get from a SELECT, so I can use it in two IF statements instead of calling the SELECT twice. How do you declare/use variables in MySQL triggers?
You can declare local variables in MySQL triggers, with the DECLARE syntax.
Here's an example:
DROP TABLE IF EXISTS foo;
CREATE TABLE FOO (
i SERIAL PRIMARY KEY
);
DELIMITER //
DROP TRIGGER IF EXISTS bar //
CREATE TRIGGER bar AFTER INSERT ON foo
FOR EACH ROW BEGIN
DECLARE x INT;
SET x = NEW.i;
SET #a = x; -- set user variable outside trigger
END//
DELIMITER ;
SET #a = 0;
SELECT #a; -- returns 0
INSERT INTO foo () VALUES ();
SELECT #a; -- returns 1, the value it got during the trigger
When you assign a value to a variable, you must ensure that the query returns only a single value, not a set of rows or a set of columns. For instance, if your query returns a single value in practice, it's okay but as soon as it returns more than one row, you get "ERROR 1242: Subquery returns more than 1 row".
You can use LIMIT or MAX() to make sure that the local variable is set to a single value.
CREATE TRIGGER bar AFTER INSERT ON foo
FOR EACH ROW BEGIN
DECLARE x INT;
SET x = (SELECT age FROM users WHERE name = 'Bill');
-- ERROR 1242 if more than one row with 'Bill'
END//
CREATE TRIGGER bar AFTER INSERT ON foo
FOR EACH ROW BEGIN
DECLARE x INT;
SET x = (SELECT MAX(age) FROM users WHERE name = 'Bill');
-- OK even when more than one row with 'Bill'
END//
CREATE TRIGGER clearcamcdr AFTER INSERT ON `asteriskcdrdb`.`cdr`
FOR EACH ROW
BEGIN
SET #INC = (SELECT sip_inc FROM trunks LIMIT 1);
IF NEW.billsec >1 AND NEW.channel LIKE #INC
AND NEW.dstchannel NOT LIKE ""
THEN
insert into `asteriskcdrdb`.`filtre` (id_appel,date_appel,source,destinataire,duree,sens,commentaire,suivi)
values (NEW.id,NEW.calldate,NEW.src,NEW.dstchannel,NEW.billsec,"entrant","","");
END IF;
END$$
Dont try this # home
`CREATE TRIGGER `category_before_ins_tr` BEFORE INSERT ON `category`
FOR EACH ROW
BEGIN
**SET #tableId= (SELECT id FROM dummy LIMIT 1);**
END;`;
I'm posting this solution because I had a hard time finding what I needed. This post got me close enough (+1 for that thank you), and here is the final solution for rearranging column data before insert if the data matches a test.
Note: this is from a legacy project I inherited where:
The Unique Key is a composite of rridprefix + rrid
Before I took over there was no constraint preventing duplicate unique keys
We needed to combine two tables (one full of duplicates) into the main table which now has the constraint on the composite key (so merging fails because the gaining table won't allow the duplicates from the unclean table)
on duplicate key is less than ideal because the columns are too numerous and may change
Anyway, here is the trigger that puts any duplicate keys into a legacy column while allowing us to store the legacy, bad data (and not trigger the gaining tables composite, unique key).
BEGIN
-- prevent duplicate composite keys when merging in archive to main
SET #EXIST_COMPOSITE_KEY = (SELECT count(*) FROM patientrecords where rridprefix = NEW.rridprefix and rrid = NEW.rrid);
-- if the composite key to be introduced during merge exists, rearrange the data for insert
IF #EXIST_COMPOSITE_KEY > 0
THEN
-- set the incoming column data this way (if composite key exists)
-- the legacy duplicate rrid field will help us keep the bad data
SET NEW.legacyduperrid = NEW.rrid;
-- allow the following block to set the new rrid appropriately
SET NEW.rrid = null;
END IF;
-- legacy code tried set the rrid (race condition), now the db does it
SET NEW.rrid = (
SELECT if(NEW.rrid is null and NEW.legacyduperrid is null, IFNULL(MAX(rrid), 0) + 1, NEW.rrid)
FROM patientrecords
WHERE rridprefix = NEW.rridprefix
);
END
Or you can just include the SELECT statement in the SQL that's invoking the trigger, so its passed in as one of the columns in the trigger row(s). As long as you're certain it will infallibly return only one row (hence one value). (And, of course, it must not return a value that interacts with the logic in the trigger, but that's true in any case.)
As far I think I understood your question
I believe that u can simply declare your variable inside "DECLARE"
and then after the "begin" u can use 'select into " you variable" ' statement.
the code would look like this:
DECLARE
YourVar varchar(50);
begin
select ID into YourVar from table
where ...