MySQL Procedure to synchronize the schema of 2 tables - mysql

The goal is to add the missing columns when comparing the schemas of two tables in MySQL 5.5 (engine MyISAM). The argument p_table1 is the model table name from which p_table2 will be compared and "synchronized".
When it is called, nothing happens, no errors, no nothing. I've tried to log some variables but it hasn't worked either.
What could be wrong with the code?
CREATE PROCEDURE synchronize_tables(p_table1 VARCHAR(64), p_table2 VARCHAR(64), p_schema_name VARCHAR(64))
BEGIN
DECLARE v_done INT default false;
DECLARE v_actual_column_name VARCHAR(64);
DECLARE v_does_columns_exist INT default true;
DECLARE v_column_type LONGTEXT;
DECLARE v_column_default LONGTEXT;
DECLARE v_is_nullable VARCHAR(3);
DECLARE v_cur CURSOR FOR
SELECT column_name
FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = p_table1
AND table_schema = p_schema_name;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = TRUE;
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
OPEN v_cur;
read_loop: LOOP
FETCH v_cur INTO v_actual_column_name;
IF v_done THEN
LEAVE read_loop;
END IF;
SELECT count(*) INTO v_does_columns_exist
FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = p_table2
AND table_schema = p_schema_name
AND column_name = v_actual_column_name;
IF NOT v_does_columns_exist THEN
SELECT column_type, COLUMN_DEFAULT, IS_NULLABLE
INTO v_column_type, v_column_default, v_is_nullable
FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = p_table1
AND table_schema = p_schema_name
AND column_name = v_actual_column_name;
SET #stmt_text = CONCAT('ALTER TABLE ', p_schema_name, '.', p_table2,
' ADD COLUMN ', v_actual_column_name, ' ', v_column_type, ' ', IF(upper(v_is_nullable) = 'NO', 'NOT NULL', ''),
' DEFAULT ', v_column_default);
prepare v_stmt FROM #stmt_text;
execute v_stmt;
deallocate prepare v_stmt;
END IF;
END LOOP;
CLOSE v_cur;
END;
END

I found a couple of problems.
First is that most of your cursor code is inside an EXIT HANDLER FOR SQLEXCEPTION. This block is run only if an error occurs. So normally this block will never be run.
Second, you CONCAT() columns to form your ALTER TABLE statement, but one or more of the columns can be NULL. When you CONCAT() any string with a null, the result of the whole concat operation is NULL. So you have to make sure NULLs are defaulted to something non-NULL.
In my test, the column default is frequently NULL. We'd want this to become the keyword "NULL" in the ALTER TABLE statement. Also if the default is not NULL, you probably want to quote it, because an ordinary default value may be a string or a date, and you aren't quoting it. The solution: QUOTE() is a builtin function that quotes strings properly, and it even turns a NULL into the keyword "NULL".
Here's what I got to work:
CREATE PROCEDURE synchronize_tables(p_table1 VARCHAR(64),
p_table2 VARCHAR(64), p_schema_name VARCHAR(64))
BEGIN
DECLARE v_done INT default false;
DECLARE v_actual_column_name VARCHAR(64);
DECLARE v_does_columns_exist INT default true;
DECLARE v_column_type LONGTEXT;
DECLARE v_column_default LONGTEXT;
DECLARE v_is_nullable VARCHAR(3);
DECLARE v_cur CURSOR FOR
SELECT column_name
FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = p_table1
AND table_schema = p_schema_name;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = TRUE;
OPEN v_cur;
read_loop: LOOP
FETCH v_cur INTO v_actual_column_name;
IF v_done THEN
LEAVE read_loop;
END IF;
SELECT count(*) INTO v_does_columns_exist
FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = p_table2
AND table_schema = p_schema_name
AND column_name = v_actual_column_name;
IF NOT v_does_columns_exist THEN
SELECT column_type, COLUMN_DEFAULT, IS_NULLABLE
INTO v_column_type, v_column_default, v_is_nullable
FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = p_table1
AND table_schema = p_schema_name
AND column_name = v_actual_column_name;
SET #stmt_text = CONCAT('ALTER TABLE ',
p_schema_name, '.', p_table2,
' ADD COLUMN ', v_actual_column_name, ' ',
v_column_type, ' ',
IF(upper(v_is_nullable) = 'NO', 'NOT NULL', ''),
' DEFAULT ', QUOTE(v_column_default));
PREPARE v_stmt FROM #stmt_text;
EXECUTE v_stmt;
DEALLOCATE prepare v_stmt;
END IF;
END LOOP;
CLOSE v_cur;
END
There are other problems with this approach:
Identifiers are not delimited.
It generates one ALTER TABLE for each missing column, even though one can add multiple columns in one ALTER.
It doesn't do the right thing when a column is NOT NULL but has no default.
However, I wouldn't do this with cursors anyway. There's an easier way:
CREATE PROCEDURE synchronize_tables(p_table1 VARCHAR(64),
p_table2 VARCHAR(64), p_schema_name VARCHAR(64))
BEGIN
SELECT CONCAT(
'ALTER TABLE `', C1.TABLE_SCHEMA, '`.`', p_table2, '` ',
GROUP_CONCAT(CONCAT(
'ADD COLUMN `', C1.COLUMN_NAME, '` ', C1.COLUMN_TYPE,
IF(C1.IS_NULLABLE='NO', ' NOT NULL ', ''),
IF(C1.COLUMN_DEFAULT IS NULL, '',
CONCAT(' DEFAULT ', QUOTE(C1.COLUMN_DEFAULT)))
) SEPARATOR ', '
)
) INTO #stmt_text
FROM INFORMATION_SCHEMA.COLUMNS AS C1
LEFT OUTER JOIN INFORMATION_SCHEMA.COLUMNS AS C2
ON C1.TABLE_SCHEMA=C2.TABLE_SCHEMA
AND C2.TABLE_NAME=p_table2
AND C1.COLUMN_NAME=C2.COLUMN_NAME
WHERE C1.TABLE_SCHEMA=p_schema_name AND C1.TABLE_NAME=p_table1
AND C2.TABLE_SCHEMA IS NULL;
PREPARE v_stmt FROM #stmt_text;
EXECUTE v_stmt;
DEALLOCATE prepare v_stmt;
END
This makes one ALTER TABLE to add all columns. It delimits identifiers. It handles NOT NULL better.
But even my solution still has problems:
Causes an error if you try to add a NOT NULL column with no DEFAULT to a populated table2.
Doesn't notice extra columns that exist in the second table but not the first table.
Doesn't synchronize constraints, indexes, triggers, procedures, or views.
It's a very complex task to sync database structure completely. I suggest that trying to use MySQL's stored procedure language for this is making a hard task even harder.
MySQL's implementation of stored procedures sucks.
Poor documentation.
No support for packages.
No standard procedures or rich function library.
No real debugger exists (some tools try, but they're faking it).
No support for persisting compiled procedures. Procedures are recompiled by every session that uses them.
I often recommend to developers who are accustomed to using procedures in Oracle or Microsoft SQL Server, to stay away from MySQL stored procedures.

Related

Mysql calling procedure failed when dynamically alter table in it

I want to alter my tables dynamically based on whether the table has specific column.
My database name is summer_cms, and there are over 50 tables in it.
What I want are below:
If a table has a column named add_time, then I would like to add a column add_user_id in it.
Similarly, I would like to add update_user_id in the table if update_time is found.
I know I should get it down in the process of creating the database schemas, but my database has been built and I have to alter it by need.
So I create a procedure to do it:
CREATE PROCEDURE ALTER_SUMMER_TABLE()
BEGIN
DECLARE tableName VARCHAR(64);
DECLARE exitence VARCHAR(64);
DECLARE ntable INT; # number of tables
DECLARE i INT; # index
SET i = 0;
# get the count of table
SELECT COUNT(DISTINCT(TABLE_NAME)) INTO ntable FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'summer_cms';
WHILE i < ntable DO
# select the specific table name into the variable of `tableName`.
SELECT TABLE_NAME INTO tableName
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = 'summer_cms'
AND COLUMN_NAME = 'add_time'
LIMIT 1 OFFSET i;
# alter table, but I get error in this clause.
ALTER TABLE tableName ADD COLUMN `add_user_id` INT NOT NULL DEFAULT 0 COMMENT 'add user id';
# check if the table has `update_time`
SELECT TABLE_NAME INTO exitence
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = 'summer_cms'
AND TABLE_NAME = tableName
AND COLUMN_NAME = 'update_time';
# add `update_user_id` if `update_time` be found.
IF exitence THEN
ALTER TABLE tableName ADD COLUMN `update_user_id` INT NOT NULL DEFAULT 0 COMMENT 'update user id';
END IF;
SET i = i + 1;
END WHILE;
END
But I got an error when I call this procedure.
Procedure execution failed
1146 - Table 'summer_cms.tableName' doesn't exist
Dose anyone could tell me what I was missing or wrong? Any help will be appreciated.
There a a few alterations you can make to your procedure to make it more streamlined as well as getting round a few problems.
First using a cursor to select the table names rather than using the two selects your using. Secondly to use a prepared statement to allow you to dynamically set the table name...
DELIMITER $$
CREATE DEFINER=`root`#`localhost` PROCEDURE `ALTER_SUMMER_TABLE`()
BEGIN
DECLARE done INT DEFAULT 0;
DECLARE tableName VARCHAR(64);
declare cur cursor for SELECT TABLE_NAME
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = 'summer_cms'
AND COLUMN_NAME = 'add_time';
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
open cur;
start_loop: loop
fetch cur into tableName;
if (done = 1 )THEN
LEAVE start_loop;
END IF;
SET #sql = CONCAT('ALTER TABLE ', tableName,' ADD COLUMN `add_user_id` INT NOT NULL DEFAULT 0 ');
PREPARE stmt FROM #sql;
EXECUTE stmt;
end loop;
close cur;
END$$
DELIMITER ;
You could do a few tweaks - only fetch table names where the column doesn't already exist for example.
Here's an example of dynamic sql
drop procedure if exists alter_table;
delimiter //
CREATE DEFINER=`root`#`localhost` procedure alter_table()
begin
declare tablename varchar(20);
set tablename = 'u';
set #sqlstmt = concat('ALTER TABLE ', tableName, ' ADD COLUMN ', char(96), 'add_user_id', char(96), ' INT NOT NULL DEFAULT 0 COMMENT', char(39), 'add user id', char(39),';');
prepare stmt from #sqlstmt;
execute stmt;
deallocate prepare stmt;
end //
delimiter ;
Note I have used ascii backticks and single quotes.

SQL - Get databases which contain a specific record

So originally I have this as a test run.
SELECT DISTINCT table_schema FROM information_schema.columns WHERE table_schema LIKE '%or';
I have looked around and found queries to show all the databases that contain a specific table.
However is it possible to have a query to go a step further and do the following:
"Select all those databases that have a particular table in them and that, in that table, have a particular record in a particular column."?
You cannot do what you want with a SQL statement.
But, you can use SQL to generate the statement that you want. The basic statement is:
select "tablename"
from tablename
where columnname = value
limit 1
Note that value may need to have single quotes around it. You can generate this with:
select concat('select "', c.table_name, '" ',
'from ', c.schema_name, '.', c.table_name, ' ',
'where ', c.column_name, ' = ', VALUE, ' '
'limit 1'
)
from information_schema.columns c
where c.table_name = TABLENAME and c.column_name = COLUMN_NAME;
To put all the statements in one long SQL statement, use:
select group_concat(concat('select "', c.table_name, '" as table_name',
'from ', c.schema_name, '.', c.table_name, ' ',
'where ', c.column_name, ' = ', VALUE, ' '
'limit 1'
) SEPARATOR ' union all '
)
from information_schema.columns c
where c.table_name = TABLENAME and c.column_name = COLUMN_NAME;
I would then just copy the resulting SQL statement and run it. If you like, you can add a prepare statement and run it dynamically.
as an example,
I have a table named T1 with columns (C1,C2), and I am searching for the value 'Needle'.
What this store procedure does is search through table names that starts with T and columns that starts with C, then loop through them and finds the value 'Needle'. It then returns the table_Schema,table_name,column_name and how many times the value 'Needle' is found within that column_name,table_name,table_schema combination.
see this sqlFiddle
CREATE PROCEDURE findDatabase(IN in_value varchar(50))
BEGIN
DECLARE bDone INT;
DECLARE _TableSchema VARCHAR(50);
DECLARE _TableName VARCHAR(50);
DECLARE _ColumnName VARCHAR(50);
DECLARE curs CURSOR FOR SELECT TABLE_SCHEMA,TABLE_NAME,COLUMN_NAME FROM information_schema.columns WHERE TABLE_NAME LIKE "T%" AND COLUMN_NAME LIKE "C%";
DECLARE CONTINUE HANDLER FOR NOT FOUND SET bDone = 1;
DROP TEMPORARY TABLE IF EXISTS tblResults;
CREATE TEMPORARY TABLE IF NOT EXISTS tblResults (
id int auto_increment primary key,
tableSchema varchar(50),
tablename varchar(50),
columnname varchar(50),
timesFound int
);
OPEN curs;
SET bDone = 0;
REPEAT
FETCH curs INTO _TableSchema,_TableName,_ColumnName;
SET #found = 0;
SET #sql = CONCAT("SET #found = (SELECT COUNT(*) FROM ",_TableSchema,".",_TableName,
" WHERE ",_ColumnName,"='",in_value,"')");
PREPARE statement FROM #sql;
EXECUTE statement;
IF (#found > 0) THEN
INSERT INTO tblResults(tableSchema,tableName,columnName,TimesFound) VALUES (_TableSchema,_TableName,_ColumnName,#found);
END IF;
UNTIL bDone END REPEAT;
CLOSE curs;
SELECT DISTINCT TableSchema,TableName,ColumnName,TimesFound FROM tblResults;
DROP TABLE tblResults;
END//

Search which tables in MySQL have rows with empty ENUMs

We are cleaning our MySQL database and finding many rows with empty string (as value) in ENUM columns. MySQL has the nasty habit of ignoring errors like that, you know.
So, since we have hundreds of tables, I was wondering if there is a way to find which tables have this problem. Solutions involving queries, procedures or phpMyAdmin commands are welcome.
Thanks in advance!
P.S. My first question here! Yay!
So, following the suggestions in the comments, I made a procedure that prints the table name, the column name and the number of rows with empty strings instead of a ENUM value.
It works in mysql terminal app, but not in phpMyAdmin.
USE __DATABASE_NAME_HERE__;
DELIMITER //
DROP PROCEDURE IF EXISTS hunt //
CREATE PROCEDURE hunt()
BEGIN
DECLARE done INT DEFAULT false;
DECLARE current_table_name VARCHAR(255);
DECLARE current_column_name VARCHAR(255);
DECLARE my_cursor CURSOR FOR
SELECT table_name, column_name
FROM information_schema.columns
WHERE information_schema.columns.table_schema = "__DATABASE_NAME_HERE__" AND information_schema.columns.column_type LIKE "%ENUM%";
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
OPEN my_cursor;
my_loop: LOOP
FETCH my_cursor INTO current_table_name, current_column_name;
IF done THEN
LEAVE my_loop;
END IF;
SET #sql = CONCAT(
' SELECT COUNT(*) as column_count, "', current_table_name, '" AS table_name, "', current_column_name, '" AS column_name',
' FROM ', current_table_name,
' WHERE `', current_column_name , '` = ""',
' HAVING column_count > 0'
);
PREPARE stmt FROM #sql;
EXECUTE stmt;
DROP PREPARE stmt;
END LOOP;
CLOSE my_cursor;
END //
DELIMITER ;
CALL hunt();

Updating empty string to NULL for entire database

I'm performing some database clean up and have noticed that there are a lot of columns that have both empty strings and NULL values in various columns.
Is it possible to write an SQL statement to update the empty strings to NULL for each column of each table in my database, except for the ones that do not allow NULL's?
I've looked at the information_schema.COLUMNS table and think that this might be the place to start.
It's not possible to do this with one simple SQL statement.
But you can do it using one statement for each column.
UPDATE TABLE SET COLUMN = NULL
WHERE LENGTH(COLUMN) = 0
or, if you want to null out the items that also have whitespace:
UPDATE TABLE SET COLUMN = NULL
WHERE LENGTH(TRIM(COLUMN)) = 0
I don't think it's possible within MySQL but certainly with a script language of your choice.
Start by getting all tables SHOW TABLES
Then for each table get the different columns and find out witch ones allow null, either with DESC TABLE, SHOW CREATE TABLE or SELECT * FROM information_schema.COLUMNS, take the one you rather parse
Then for each column that allows null run a normal update that changes "" to null.
Prepare to spend some time waiting :)
I figured out how to do this using a stored procedure. I'd definitely look at using a scripting language next time.
DROP PROCEDURE IF EXISTS settonull;
DELIMITER //
CREATE PROCEDURE settonull()
BEGIN
DECLARE done INT DEFAULT FALSE;
DECLARE _tablename VARCHAR(255);
DECLARE _columnname VARCHAR(255);
DECLARE cur1 CURSOR FOR SELECT
CONCAT(TABLE_SCHEMA, '.', TABLE_NAME) AS table_name,
COLUMN_NAME AS column_name
FROM information_schema.COLUMNS
WHERE IS_NULLABLE = 'YES'
AND TABLE_SCHEMA IN ('table1', 'table2', 'table3');
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
OPEN cur1;
read_loop: LOOP
FETCH cur1 INTO _tablename, _columnname;
IF done THEN
LEAVE read_loop;
END IF;
SET #s = CONCAT('UPDATE ', _tablename, ' SET ', _columnname, ' = NULL WHERE LENGTH(TRIM(', _columnname, ')) = 0' );
PREPARE stmt FROM #s;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END LOOP;
CLOSE cur1;
END//
DELIMITER ;
CALL settonull();

Show table name where a value is present

Is it possible to show the name of a table in a db where a specific value is present. I have different tables and i want to show only the table names that contains a specific value in any of the fields.
This will return lots of empty result sets, but the non-empty ones correspond to table/column combinations that fit your search. It only works for text, and detects columns that contain the value (as opposed to a full column match.)
DELIMITER |
DROP PROCEDURE IF EXISTS `SearchAllTables`|
CREATE PROCEDURE `SearchAllTables` (
IN _search varchar(256)
)
LANGUAGE SQL
DETERMINISTIC
SQL SECURITY DEFINER
BEGIN
-- declare stuff
declare _tableName varchar(64);
declare _columnName varchar(64);
declare _done tinyint(1) default 0;
-- we will examine every string column in the database
declare _columnCursor cursor for
select TABLE_NAME, COLUMN_NAME
from INFORMATION_SCHEMA.COLUMNS
where TABLE_SCHEMA = database()
and (DATA_TYPE like '%char%'
or DATA_TYPE like 'text');
declare CONTINUE handler for NOT FOUND
SET _done = 1;
OPEN _columnCursor;
LOOP1: LOOP
-- get the next table/column combination
FETCH _columnCursor INTO _tableName,_columnName;
IF _done = 1 THEN
CLOSE _columnCursor;
LEAVE LOOP1;
END IF;
-- query the current column to see if it holds the value
SET #query = concat(
"select '",_tableName,"' as TableName, '",
_columnName,"' as ColumnName
from ",_tableName,"
where ",_columnName," like concat('%',?,'%')
group by 1;"
);
SET #search = _search;
PREPARE _stmt FROM #query;
EXECUTE _stmt USING #search;
DEALLOCATE PREPARE _stmt;
END LOOP LOOP1;
END|
DELIMITER ;
Oh, yeah, and it's ugly... Maybe it'll help you, though!
SELECT TABLE_NAME
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = 'database_name'
AND COLUMN_NAME = 'column_name'