How to translate PostgreSQL "merge_db" (aka upsert) function into MySQL - mysql

Straight from the manual, here's the canonical example of merge_db in PostgreSQL:
CREATE TABLE db (a INT PRIMARY KEY, b TEXT);
CREATE FUNCTION merge_db(key INT, data TEXT) RETURNS VOID AS
$$
BEGIN
LOOP
-- first try to update the key
UPDATE db SET b = data WHERE a = key;
IF found THEN
RETURN;
END IF;
-- not there, so try to insert the key
-- if someone else inserts the same key concurrently,
-- we could get a unique-key failure
BEGIN
INSERT INTO db(a,b) VALUES (key, data);
RETURN;
EXCEPTION WHEN unique_violation THEN
-- Do nothing, and loop to try the UPDATE again.
END;
END LOOP;
END;
$$
LANGUAGE plpgsql;
SELECT merge_db(1, 'david');
SELECT merge_db(1, 'dennis');
Can this be expressed as a user-defined function in MySQL, and if so, how? Would there be any advantage over MySQL's standard INSERT...ON DUPLICATE KEY UPDATE?
Note: I'm specifically looking for a user-defined function, not INSERT...ON DUPLICATE KEY UPDATE.

Tested on MySQL 5.5.14.
CREATE TABLE db (a INT PRIMARY KEY, b TEXT);
DELIMITER //
CREATE PROCEDURE merge_db(k INT, data TEXT)
BEGIN
DECLARE done BOOLEAN;
REPEAT
BEGIN
-- If there is a unique key constraint error then
-- someone made a concurrent insert. Reset the sentinel
-- and try again.
DECLARE ER_DUP_UNIQUE CONDITION FOR 23000;
DECLARE CONTINUE HANDLER FOR ER_DUP_UNIQUE BEGIN
SET done = FALSE;
END;
SET done = TRUE;
SELECT COUNT(*) INTO #count FROM db WHERE a = k;
-- Race condition here. If a concurrent INSERT is made after
-- the SELECT but before the INSERT below we'll get a duplicate
-- key error. But the handler above will take care of that.
IF #count > 0 THEN
UPDATE db SET b = data WHERE a = k;
ELSE
INSERT INTO db (a, b) VALUES (k, data);
END IF;
END;
UNTIL done END REPEAT;
END//
DELIMITER ;
CALL merge_db(1, 'david');
CALL merge_db(1, 'dennis');
Some thoughts:
You can't do an update first and then check #ROW_COUNT() because it returns the number of rows actually changed. This could be 0 if the row already has the value you are trying to update.
Also, #ROW_COUNT() is not replication safe.
You could use REPLACE...INTO.
If using InnoDB or a table with transaction support you might be able to use SELECT...FOR UPDATE (untested).
I see no advantage to this solution over just using INSERT...ON DUPLICATE KEY UPDATE.

Related

PLsql equivalent query from PLpgsql

I am new to mysql and used to work in postgresql. I have been trying to make a function in mysql workbench using plsql. I have written a code in plpgsql to describe what I want to do (basically data entry in a table avoiding the redundant entries)
I tried searching relevant answers and founds ways to delete duplicate entries from table. But, what I want to do is avoid duplicate data entry in the table. Following is the workable code but I don't understand how to get out of the loop if the update query runs successfully, if not, then insert query needs to be run. I know I can add IF-then-ELSE here. but what to write in the conditions.
DELIMITER //
CREATE PROCEDURE merge_abc
(IN data CHAR(20))
BEGIN
LOOP
UPDATE abc SET node_name = data WHERE node_name = data;
END LOOP;
END //
DELIMITER ;
*Aim is to avoid duplicate data entry rather than deleting the duplicate entries in the end just like this;
CREATE FUNCTION merge_abc(data TEXT) RETURNS VOID AS
BEGIN
LOOP
-- first try to update the key
UPDATE abc SET node_name = data WHERE node_name = data;
IF found THEN
RETURN;
END IF;
-- not there, so try to insert the key
-- if someone else inserts the same key concurrently,
-- we could get a unique-key failure
BEGIN
INSERT INTO abc(node_name) VALUES (data);
RETURN;
EXCEPTION WHEN unique_violation THEN
-- Do nothing, and loop to try the UPDATE again.
END;
END LOOP;
END;

Transaction and stored procedure. Why log table is empty in this case?

I am very new in Mysql, probably don't know or don't understand something essential.
Could you please advise me why 'begin !!!' message is not inserted in this
case?
DELIMITER $$
CREATE TABLE `_debugLogTable` (
`Message` varchar(255) DEFAULT NULL
) ENGINE=InnoDB $$
CREATE PROCEDURE `debug_msg`(msg VARCHAR(255))
BEGIN
insert into _debugLogTable select msg;
END$$
CREATE FUNCTION `ValueMeetsCondition`(value varchar(20)) RETURNS tinyint(1)
BEGIN
DECLARE ConditionValue INTEGER;
call debug_msg('begin !!!');
SET ConditionValue = CAST(`value` AS UNSIGNED);
call debug_msg('end !!!');
RETURN TRUE;
END$$
DELIMITER ;
I am aware that CAST function fails, but why call debug_msg('begin !!!'); does not insert new record into table?! There are not any transactions there!
Just want post an answer, maybe it will help somebody in the future.
From this we have -
If autocommit mode is enabled, each SQL statement forms a single transaction on its own. By default, MySQL starts the session for each new connection with autocommit enabled, so MySQL does a commit after each SQL statement if that statement did not return an error. If a statement returns an error, the commit or rollback behavior depends on the error
I call function in this way - select ValueMeetsCondition('>10').
So actually it is wrapped into transaction by MySQL, that's why if something inside my procedure fails - the whole changes are roll backed.
If i remake my query in this way the message begin !! will be inserted, while end !! does not
call debug_msg('begin !!!');
SET ConditionValue = CAST(`>10` AS UNSIGNED);
select ConditionValue;
call debug_msg('end !!!');

Handling sql errors (if exists, if constraint), Perl

I am using perl and mysql on my site.
Is there a mechanism to restrict deleting if data connected to 'deleting' exists? Yes - foreign key constraint. Is it possible to return some code if row wasn't deleted because of foreign key constraint? Somethink like this:
$id = $cgi->param("id");
$query="delete from `Class` where `id` = '$id'";
$sth = $dbh->prepare($query);
$sth->execute or die(print $sth->errstr);
if ($sth->errcode eq '777')
{
print 'error! there are 1 or more rows, connected with row you want to delete';
}
else
{
print 'ok! deleted';
}
Same for inserting row and there are row with same data existing. Sure It is possible to create trigger on insert and try to return some code. Give me example of such trigger, please? And again : how to handle this error?
Some help please! Sorry for my not very good english :)
If you want to return your own success/failure code then you can use procedures. create a procedure
delimiter $$
CREATE PROCEDURE `deleteProcedure`(in Id INT)
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
select 0; -- number that you want to return on failure
ROLLBACK;
END;
START TRANSACTION;
-- put any number of statement, including delete, insert, update
delete from `Class` where `id` = Id;
COMMIT;
select 1; -- number that you want to return on success
END$$
call this procedure from your perl script, and it will return the values you put in your procedure.

MySQL EXIT HANDLER is not catching all the errors?

My MySQL procedure looks like:
create procedure create_feed (_id int)
begin
declare exit handler for sqlexception
begin
rollback;
select false;
end;
start transaction;
insert into t1(id)
values (_id);
insert into wrong_table_name (id, createdtime)
values (
_id,
CURRENT_TIMESTAMP
);
commit;
select true;
end//
After I called this procedure, the t1 table is updated, and the value 'true' is returned. The wrong_table_name does not exist at all. Why?
I recommend that you use a function instead of a procedure if you want it to return a value. Another option is to use out parameters if you want your procedure to return one or more values.
Nonetheless, I am surprised by your results. If wrong_table_name does not exist, then that proc should return true. As for t1 being updated, that would happen if you are using a non-transactional storage engine like MyISAM, which ignores the rollback.
I tested your code in MySQL 5.5.8, and it worked properly for me. Namely, it always entered the exit handler when wrong_table_name did not exist.

Convert Oracle stored procedure using REF_CURSOR and package global variable to Postgresql or MySQL

This package uses two unique features of Oracle, REF_CURSOR and a package global variable. I would like to port the functionality from Oracle to Postgresql or MySQL.
PACKAGE tox IS
/*=======================*/
g_spool_key spool.key%TYPE := NULL;
TYPE t_spool IS REF CURSOR RETURN spool%ROWTYPE;
/*=======================*/
PROCEDURE begin_spool;
/*=======================*/
PROCEDURE into_spool
(
in_txt IN spool.txt%TYPE
);
/*=======================*/
PROCEDURE reset_spool;
/*=======================*/
FUNCTION end_spool
RETURN t_spool;
/*=======================*/
FUNCTION timestamp
RETURN VARCHAR2;
/*=======================*/
END tox;
PACKAGE BODY tox
IS
/*========================================================================*/
PROCEDURE begin_spool
AS
/*=======================*/
BEGIN
/*=======================*/
SELECT
key.NEXTVAL
INTO
g_spool_key
FROM
DUAL;
/*=======================*/
END begin_spool;
/*========================================================================*/
PROCEDURE into_spool
(
in_txt IN spool.txt%TYPE
)
AS
/*=======================*/
BEGIN
/*=======================*/
INSERT INTO
spool
VALUES
(
g_spool_key,
in_txt,
seq.NEXTVAL
);
/*=======================*/
END into_spool;
/*========================================================================*/
PROCEDURE reset_spool
AS
/*=======================*/
BEGIN
/*=======================*/
DELETE
spool
WHERE
key = g_spool_key;
COMMIT;
begin_spool;
/*=======================*/
END reset_spool;
/*========================================================================*/
FUNCTION end_spool
RETURN t_spool
AS
v_spool t_spool;
/*=======================*/
BEGIN
/*=======================*/
COMMIT;
OPEN v_spool FOR
SELECT
*
FROM
spool
WHERE
key = g_spool_key
ORDER BY
seq;
RETURN v_spool;
/*=======================*/
END end_spool;
/*========================================================================*/
FUNCTION timestamp
RETURN VARCHAR2
AS
/*-----------------------*/
v_result VARCHAR2(14);
/*=======================*/
BEGIN
/*=======================*/
SELECT
TO_CHAR(SYSDATE,'YYYYMMDDHH24MISS')
INTO
v_result
FROM
DUAL;
RETURN v_result;
/*=======================*/
END timestamp;
/*========================================================================*/
END tox;
Can you produce the equivalent code? for Postgresql? for MySQL?
Note: The Oracle code is thread safe. This is a key feature.
PostgreSQL 8.3
The problem in PostgreSQL is the lack of global (or package) variables, so that part has to be solved with a temp-table that is created first. The rest of it was quite easy.
If you are serious about porting the application over to PostgreSQL or MySQL, I would recommend you to not use global variables at all since they are bad practice when coding (according to me at least :))
But anyway, here is the code:
This has to exist before running the functions:
create table spool (key integer, txt varchar(2048), seq integer);
create sequence s_key;
create sequence s_seq;
create schema tox;
create temp table globals (name varchar(10), value varchar(100), primary key(name));
The functions are being put in the schema tox to simulate a package.
create or replace function tox.get_variable(var_name varchar) returns varchar as $$
declare
ret_val varchar(100);
begin
select value into ret_val from globals where name = var_name;
return ret_val;
end
$$ language plpgsql;
create or replace function tox.set_variable(var_name varchar, value anyelement) returns void as $$
begin
delete from globals where name = var_name;
insert into globals values(var_name, value);
end;
$$ language plpgsql;
create or replace function tox.begin_spool() returns integer as $$
begin
perform tox.set_variable('key', nextval('s_key')::varchar);
return tox.get_variable('key');
end;
$$ language plpgsql;
create or replace function tox.reset_spool() returns integer as $$
begin
delete from spool where key = tox.get_variable('key')::integer;
return tox.begin_spool();
end;
$$ language plpgsql;
create or replace function tox.into_spool(in_txt spool.txt%TYPE) returns void as $$
begin
insert into spool values(tox.get_variable('key')::integer, in_txt, nextval('s_seq'));
end;
$$ language plpgsql;
create or replace function tox.end_spool(refcursor) returns refcursor as $$
declare
begin
open $1 for select * from spool where key = tox.get_variable('key')::integer order by seq;
return $1;
end;
$$ language plpgsql;
create or replace function tox.test(txt varchar(100)) returns setof spool as $$
declare
v_spool_key integer;
cnt integer;
begin
v_spool_key = tox.begin_spool();
for cnt in 1..10 loop
perform tox.into_spool(txt || cnt);
end loop;
perform tox.end_spool('spool_cursor');
return query fetch all from spool_cursor;
end;
$$ language plpgsql;
To test, just run this after everything have been created.
select * from tox.test('Test');
For mysql:
For ref_cursor you can just use a regular select in a procedure. Mysql has an implicit result set that is returned from stored procedure if you issue a select statement. See my answer here.
For the package global variable, you can put it in a table, but it appears from your code that it is a sequence, so it can be replaced with an auto_increment field. That should be pretty simple.
It would help if you can post the definition of your spool table in the question. Then I could probably provide you with exact code for mysql.
I have a hard time understanding several things in your code. It looks like you have a table with two sequences, but only one of them is truly an auto_increment column.
In mysql auto_increment is allowed only on one column in a table. have you considered making the other column a foreign key to an auto incremented column of another table?
The global variable is tricky, because mysql doesn't have them. I think the only resolution is to store it as a scalar in a table, and then tie your data to it with a foreign key.
Finally, returning a ref cursor is easy, as I pointed out in my previous answer. In the link provide (to a different answer) you can see a code sample.
Here's a solution tested with MySQL 5.1.30.
Regarding your requirement for thread-safety, the MySQL User Variable mechanism should help. This allows you to SET a variable whose state is limited to the current session. Other sessions can also create a variable by the same name, and keep a different value in it.
I assume by thread-safety you mean something like this -- session-scoped state. Because you can't really have more fine-grained thread-safe state in a database. Each thread of your application must have its own session to the database.
There are no packages in MySQL, so the user variable is global to the session. Another stored procedure that happens to use a variable of the same name will conflict.
CREATE TABLE spool (
`key` INT,
txt VARCHAR(2048),
seq INT AUTO_INCREMENT PRIMARY KEY
);
CREATE TABLE spool_key (
`key` INT AUTO_INCREMENT PRIMARY KEY
);
DELIMITER $$
CREATE PROCEDURE begin_spool ()
BEGIN
DELETE FROM spool_key;
INSERT INTO spool_key (`key`) VALUES (DEFAULT);
SET #sp_key = LAST_INSERT_ID();
END $$
CREATE PROCEDURE into_spool(IN in_txt VARCHAR(2048))
BEGIN
INSERT INTO spool (`key`, txt, seq) VALUES
(#sp_key, in_txt, DEFAULT);
END $$
CREATE PROCEDURE reset_spool()
BEGIN
DELETE spool FROM spool JOIN spool_key USING (`key`);
CALL begin_spool();
END $$
CREATE PROCEDURE end_spool()
BEGIN
SELECT *
FROM spool JOIN spool_key USING (`key`)
ORDER BY seq;
END $$
DELIMITER ;
CALL begin_spool();
CALL into_spool('now is the time');
CALL into_spool('for all good men');
CALL end_spool();
CALL reset_spool();
CALL into_spool('to come to the aid');
CALL into_spool('of their country');
CALL end_spool();
DROP FUNCTION IF EXISTS fmt_timestamp;
CREATE FUNCTION fmt_timestamp() RETURNS CHAR(14)
RETURN DATE_FORMAT(SYSDATE(), '%Y%m%d%H%i%s');
SELECT fmt_timestamp();