Author: arkurth
Date: Thu Oct 30 22:06:00 2014
New Revision: 1635628

URL: http://svn.apache.org/r1635628
Log:
VCL-764
Added upgrade_database.pl script.

Added version comment to vcl.sql. This is used by upgrade_database.pl to verify 
the file is correct. Rearranged insert statements in vcl.sql so that they work 
if constraints are already in place.

Added:
    vcl/trunk/managementnode/bin/upgrade_database.pl
Modified:
    vcl/trunk/mysql/vcl.sql

Added: vcl/trunk/managementnode/bin/upgrade_database.pl
URL: 
http://svn.apache.org/viewvc/vcl/trunk/managementnode/bin/upgrade_database.pl?rev=1635628&view=auto
==============================================================================
--- vcl/trunk/managementnode/bin/upgrade_database.pl (added)
+++ vcl/trunk/managementnode/bin/upgrade_database.pl Thu Oct 30 22:06:00 2014
@@ -0,0 +1,2277 @@
+#!/usr/bin/perl -w
+###############################################################################
+# $Id:  $
+###############################################################################
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+###############################################################################
+
+=head1 NAME
+
+VCL::vcld - VCL upgrade_database
+
+=head1 SYNOPSIS
+
+ perl ./upgrade_database.pl
+
+=head1 DESCRIPTION
+ 
+
+=cut
+
+##############################################################################
+package VCL::upgrade_database;
+use strict;
+use warnings;
+
+# Specify the lib path using FindBin
+use FindBin;
+use lib "$FindBin::Bin/../lib";
+
+# Specify the version of this module
+our $VERSION = '2.4';
+
+# Specify the version of Perl to use
+use 5.008000;
+
+use Cwd qw(abs_path);
+use File::Basename qw(fileparse);
+use File::Temp qw(tempfile);
+use Getopt::Long;
+use Storable qw(store retrieve);
+
+use VCL::utils;
+use VCL::Module;
+
+
+##############################################################################
+
+my $DEBUG = 0;
+GetOptions(\%OPTIONS,
+       'debug!' => \$DEBUG,
+);
+
+$| = 1;
+
+$VERBOSE = 1;
+$DAEMON_MODE = 0;
+
+my $DATABASE_SERVER = $SERVER;
+my $DATABASE_USERNAME = $WRTUSER;
+my $DATABASE_PASSWORD = $WRTPASS;
+
+my $RENAME_COLUMNS = {
+       'vmprofile' => {
+               'eth0generated' => 'vmware_mac_eth0_generated',
+               'eth1generated' => 'vmware_mac_eth1_generated',
+       }
+};
+
+my $VCL_SCHEMA_PATHS = {
+       'vclimport' => 
'https://svn.apache.org/repos/asf/vcl/tags/import/mysql/vcl.sql',
+       'vcl20'     => 
'https://svn.apache.org/repos/asf/vcl/tags/VCL-2.0.0/mysql/vcl.sql',
+       'vcl21'     => 
'https://svn.apache.org/repos/asf/vcl/tags/release-2.1/mysql/vcl.sql',
+       'vcl22'     => 
'https://svn.apache.org/repos/asf/vcl/tags/release-2.2/mysql/vcl.sql',
+       'vcl23'     => 
'https://svn.apache.org/repos/asf/vcl/tags/release-2.3/mysql/vcl.sql',
+       'vcl231'    => 
'https://svn.apache.org/repos/asf/vcl/tags/release-2.3.1/mysql/vcl.sql',
+       'vcltrunk'  => 
'https://svn.apache.org/repos/asf/vcl/trunk/mysql/vcl.sql',
+};
+
+my $timestamp = convert_to_datetime();
+$timestamp =~ s/:/-/g;
+$timestamp =~ s/\s/_/g;
+
+#------------------------------------------------------------------------------
+
+# Preliminary checks
+if (!get_mn_os()) {
+       setup_print_error("Failed to initialize object to interact with this 
management node's operating system. Check log file for more information:\n" . 
abs_path($LOGFILE));
+       exit 1;
+}
+
+setup();
+
+#create_test_databases() || exit;
+#upgrade_test_databases() || exit;
+
+exit;
+
+#/////////////////////////////////////////////////////////////////////////////
+
+=head2 get_mn_os
+
+ Parameters  : none
+ Returns     : OS module object reference
+ Description : Retrieves an OS module object used to interact with the
+               management node.
+
+=cut
+
+sub get_mn_os {
+       if (defined($ENV{mn_os})) {
+               return $ENV{mn_os};
+       }
+       
+       # Create an OS object to control this management node
+       my $mn_os = VCL::Module::create_mn_os_object();
+       if (!$mn_os) {
+               setup_print_error("failed to create OS object to control this 
management node");
+               exit 1;
+       }
+       $ENV{mn_os} = $mn_os;
+       return $mn_os;
+}
+
+#/////////////////////////////////////////////////////////////////////////////
+
+=head2 verify_vcl_sql_file
+
+ Parameters  : $source_sql_file_path
+ Returns     : boolean
+ Description : Performs various checks on an SQL file used to upgrade a VCL
+               database. The file must:
+               - exist
+               - be named 'vcl.sql'
+               - contain a 'Version:' line
+               - the version line must match the $VERSION variable of this
+                 script
+
+=cut
+
+sub verify_vcl_sql_file {
+       my ($source_sql_file_path) = @_;
+       if (!$source_sql_file_path) {
+               notify($ERRORS{'WARNING'}, 0, "source VCL SQL file path 
argument was not supplied");
+               return;
+       }
+       
+       my $mn_os = get_mn_os();
+       
+       if ($source_sql_file_path !~ m|/vcl.sql$|) {
+               setup_print_warning("file must be named vcl.sql");
+               return;
+       }
+       
+       if (!$mn_os->file_exists($source_sql_file_path)) {
+               setup_print_warning("file does not exist: 
$source_sql_file_path");
+               return;
+       }
+       
+       my @lines = $mn_os->get_file_contents($source_sql_file_path);
+       if (!@lines) {
+               setup_print_error("unable to retrieve contents of file: 
$source_sql_file_path");
+               return;
+       }
+       
+       #-- Version: x.x
+       my ($version_line) = grep(/--\s+Version:/, @lines);
+       if (!$version_line) {
+               setup_print_error("unable to verify file: 
$source_sql_file_path, it does not contain a 'Version:' line");
+               return;
+       }
+       
+       my ($version) = $version_line =~ /Version:\s+([\d\.]+)/s;
+       if (!$version) {
+               setup_print_error("unable to verify file: 
$source_sql_file_path, version line could not be parsed:\n$version_line");
+               return;
+       }
+       elsif ($version ne $VERSION) {
+               setup_print_error("unable to verify file: 
$source_sql_file_path, version inside the file '$version' does not match the 
version of this script '$VERSION'");
+               return;
+       }
+       
+       print "verified VCL $VERSION database schema file: 
$source_sql_file_path\n";
+       return 1;
+}
+
+#/////////////////////////////////////////////////////////////////////////////
+
+=head2 create_database
+
+ Parameters  : $database_name
+ Returns     : boolean
+ Description : Creates a database using the credentials and database server
+               specified in vcld.conf.
+
+=cut
+
+sub create_database {
+       my ($database_name) = @_;
+       if (!$database_name) {
+               notify($ERRORS{'WARNING'}, 0, "database name argument was not 
specified");
+               return;
+       }
+       
+       print "attempting to create '$database_name' database on 
$DATABASE_SERVER\n";
+       
+       my $mn_os = get_mn_os();
+       if (!$mn_os) {
+               setup_print_error("unable to create database, failed to 
retrieve OS object to control this management node");
+               return;
+       }
+       
+       my $command = "mysql -h $DATABASE_SERVER -u $DATABASE_USERNAME 
--password='$DATABASE_PASSWORD' -e 'CREATE DATABASE $database_name;'";
+       my ($exit_status, $output) = $mn_os->execute($command);
+       if (!defined($output)) {
+               setup_print_error("failed to execute command to create database 
on $DATABASE_SERVER: $command");
+               return;
+       }
+       
+       # Check for access denied error:
+       # ERROR 1044 (42000) at line 1: Access denied for user 
'<username>'@'<IP address>' to database '<database name>'
+       elsif (my ($access_denied_line) = grep(/Access denied/i, @$output)) {
+               setup_print_error("failed to create '$database_name' database 
on database server $DATABASE_SERVER because the database user does not have the 
CREATE privilege.");
+               my ($username, $source_host) = $access_denied_line =~ 
/'([^']+)'\@'([^']+)'/;
+               if ($username && $source_host) {
+                       print "\nexecute the following command on the database 
server:\n";
+                       print "mysql -e \"GRANT CREATE ON *.* TO 
'$username'\@'$source_host';\"\n";
+               }
+               return;
+       }
+       
+       # Check for database already exists error
+       # ERROR 1007 (HY000) at line 1: Can't create database '<database 
name>'; database exists
+       elsif (grep(/database exists/i, @$output)) {
+               setup_print_error("failed to create '$database_name' database 
on $DATABASE_SERVER because a database with this name already exists");
+               return 0;
+       }
+       
+       elsif ($exit_status != 0 || grep(/ERROR/i, @$output)) {
+               setup_print_error("failed to create '$database_name' database 
on $DATABASE_SERVER, exit status: $exit_status, output:\n" . join("\n", 
@$output) . "");
+               return 0;
+       }
+       else {
+               print "created '$database_name' database on $DATABASE_SERVER\n";
+               return 1;
+       }
+}
+
+#/////////////////////////////////////////////////////////////////////////////
+
+=head2 drop_database
+
+ Parameters  : $database_name
+ Returns     : boolean
+ Description : Drops a database using the credentials and database server
+               specified in vcld.conf.
+
+=cut
+
+sub drop_database {
+       my ($database_name) = @_;
+       if (!$database_name) {
+               notify($ERRORS{'WARNING'}, 0, "database name argument was not 
specified");
+               return;
+       }
+       
+       print "attempting to drop '$database_name' database on 
$DATABASE_SERVER\n";
+       
+       my $mn_os = get_mn_os();
+       if (!$mn_os) {
+               setup_print_error("unable to drop database, failed to retrieve 
OS object to control this management node");
+               return;
+       }
+       
+       my $command = "mysql -h $DATABASE_SERVER -u $DATABASE_USERNAME 
--password='$DATABASE_PASSWORD' -e 'drop DATABASE $database_name'";
+       my ($exit_status, $output) = $mn_os->execute($command);
+       if (!defined($output)) {
+               setup_print_error("failed to execute command to drop database 
on $DATABASE_SERVER: $command");
+               return;
+       }
+       # Check for access denied error:
+       # ERROR 1044 (42000) at line 1: Access denied for user 
'<username>'@'<IP address>' to database '<database>'
+       elsif (my ($access_denied_line) = grep(/Access denied/i, @$output)) {
+               setup_print_error("failed to drop '$database_name' database on 
database server $DATABASE_SERVER because the database user does not have the 
DROP privilege.");  
+               my ($username, $source_host) = $access_denied_line =~ 
/'([^']+)'\@'([^']+)'/;
+               if ($username && $source_host) {
+                       print "\nexecute the following command on the database 
server:\n";
+                       print "mysql -e \"GRANT DROP ON $database_name.* TO 
'$username'\@'$source_host';\"\n";
+               }
+               return;
+       }
+       #ERROR 1008 (HY000) at line 1: Can't drop database 'vcl_import'; 
database doesn't exist
+       elsif (grep(/ERROR 1008/i, @$output)) {
+               print "'$database_name' database does not exist on database 
server $DATABASE_SERVER\n";
+               return 1;
+       }
+       
+       else {
+               print "dropped '$database_name' database on $DATABASE_SERVER\n";
+               return 1;
+       }
+}
+
+#/////////////////////////////////////////////////////////////////////////////
+
+=head2 import_sql_file
+
+ Parameters  : $database_name, $sql_file_path
+ Returns     : boolean
+ Description : Imports an SQL file into a database.
+
+=cut
+
+sub import_sql_file {
+       my ($database_name, $sql_file_path) = @_;
+       if (!$database_name || !$sql_file_path) {
+               notify($ERRORS{'WARNING'}, 0, "database name and SQL file path 
arguments were not specified");
+               return;
+       }
+       
+       my $mn_os = get_mn_os();
+       if (!$mn_os) {
+               setup_print_error("unable to import SQL file, failed to 
retrieve OS object to control this management node");
+               return;
+       }
+       
+       my $command = "mysql -h $DATABASE_SERVER -u $DATABASE_USERNAME 
--password='$DATABASE_PASSWORD' $database_name < $sql_file_path";
+       print "attempting to import $sql_file_path into '$database_name' 
database\n";
+       my ($exit_status, $output) = $mn_os->execute($command);
+       if (!defined($output)) {
+               setup_print_error("failed to execute command to import 
$sql_file_path into '$database_name' database on $DATABASE_SERVER");
+               print "command:\n$command\n";
+               return;
+       }
+       elsif ($exit_status != 0 || grep(/ERROR/i, @$output)) {
+               setup_print_error("failed to import import $sql_file_path into 
'$database_name' database on $DATABASE_SERVER, output:");
+               print join("\n", @$output) . "\n";
+               return 0;
+       }
+       else {
+               print "imported $sql_file_path into '$database_name' 
database\n";
+               return 1;
+       }
+}
+
+#/////////////////////////////////////////////////////////////////////////////
+
+=head2 create_test_databases
+
+ Parameters  : @database_keys (optional)
+ Returns     : boolean
+ Description :
+
+=cut
+
+sub create_test_databases {
+       my $mn_os = get_mn_os();
+       if (!$mn_os) {
+               setup_print_error("failed to retrieve OS object to control this 
management node");
+               return;
+       }
+       
+       my @database_keys = @_;
+       if (!@database_keys) {
+               @database_keys = keys %$VCL_SCHEMA_PATHS
+       }
+       
+       for my $database_name (@database_keys) {
+               setup_print_break();
+               print "creating test database: $database_name\n\n";
+               
+               my $sql_file_url = $VCL_SCHEMA_PATHS->{$database_name};
+               my $sql_temp_file_path = '/tmp/vcl.sql';
+               my $sql_file_path = "/tmp/$database_name.sql";
+               
+               if ($mn_os->file_exists($sql_file_path)) {
+                       $mn_os->delete_file($sql_file_path);
+               }
+               
+               print "downloading VCL schema file: $sql_file_url\n";
+               
+               my $wget_command = "wget -N -P /tmp $sql_file_url";
+               my ($wget_exit_status, $wget_output) = 
$mn_os->execute($wget_command);
+               if (!defined($wget_output)) {
+                       setup_print_error("failed to execute command to 
download VCL schema file: $wget_command");
+                       return;
+               }
+               elsif ($wget_exit_status ne '0') {
+                       setup_print_error("failed to download VCL schema file, 
exit status: $wget_exit_status\n");
+                       print "command: $wget_command\n";
+                       print "output:\n" . join("\n", @$wget_output) . "\n";
+                       return;
+               }
+               else {
+                       print "downloaded VCL schema file: $sql_file_url --> 
$sql_temp_file_path\n";
+               }
+               
+               if ($mn_os->move_file($sql_temp_file_path, $sql_file_path)) {
+                       print "renamed file: $sql_temp_file_path --> 
$sql_file_path\n";
+               }
+               else {
+                       setup_print_error("failed to rename file: 
$sql_temp_file_path --> $sql_file_path");
+                       return;
+               }
+               
+               print "\n";
+               if (!drop_database($database_name)) {
+                       return;
+               }
+               
+               print "\n";
+               if (!create_database($database_name)) {
+                       return;
+               }
+               
+               print "\n";
+               if (!import_sql_file($database_name, $sql_file_path)) {
+                       return;
+               }
+       }
+       
+       return 1;
+}
+
+#/////////////////////////////////////////////////////////////////////////////
+
+=head2 upgrade_test_databases
+
+ Parameters  : $reference_sql_file_path, @database_keys (optional)
+ Returns     : boolean
+ Description :
+
+=cut
+
+sub upgrade_test_databases {
+       my $mn_os = get_mn_os();
+       if (!$mn_os) {
+               setup_print_error("failed to retrieve OS object to control this 
management node");
+               return;
+       }
+       
+       my ($reference_sql_file_path, @database_keys) = @_;
+       if (!$reference_sql_file_path) {
+               setup_print_error("reference SQL file path argument was not 
provided");
+               return;
+       }
+       
+       if (!@database_keys) {
+               @database_keys = keys %$VCL_SCHEMA_PATHS
+       }
+       
+       my $dumped_trunk = 0;
+       my @diff_commands;
+       my @diff_sort_file_paths;
+       for my $database_name ('vcltrunk', @database_keys) {
+               if ($database_name eq 'vcltrunk' && $dumped_trunk) {
+                       next;
+               }
+               
+               setup_print_break();
+               
+               if ($database_name ne 'vcltrunk') {
+                       print "upgrading test database: $database_name\n";
+                       setup_upgrade_database($database_name, 
$reference_sql_file_path) || return;
+                       print "upgraded test database: $database_name\n";
+               }
+               
+               my $database_dump_sql_file_path = 
"/tmp/$database_name\_dump.sql";
+               dump_database_to_file($database_name, 
$database_dump_sql_file_path, '--no-data');
+               
+               # Remove comments from dumped file - makes it easier to diff
+               `sed -i 's/\\/\\*.*//' $database_dump_sql_file_path`;
+               `sed -i 's/AUTO_INCREMENT=[0-9]* //' 
$database_dump_sql_file_path`;
+               
+               `sort $database_dump_sql_file_path > 
$database_dump_sql_file_path.sort`;
+               `sed -i -e 's/,\$//' $database_dump_sql_file_path.sort`;
+               `sed -i -e 's/^USE.*//' $database_dump_sql_file_path.sort`;
+               `sed -i -e 's/^mysqldump.*//' 
$database_dump_sql_file_path.sort`;
+               
+               if ($database_name eq 'vcltrunk') {
+                       $dumped_trunk = 1;
+                       next;
+               }
+               
+               my $diff_file_path = "$database_dump_sql_file_path.diff";
+               my $diff_command = "diff -W 200 -w -B --side-by-side 
--suppress-common-lines $database_dump_sql_file_path /tmp/vcltrunk_dump.sql";
+               push @diff_commands, $diff_command,
+               `$diff_command > $diff_file_path`;
+               
+               my $sort_diff_command = "diff -W 200 -w -B --side-by-side 
--suppress-common-lines $database_dump_sql_file_path.sort 
/tmp/vcltrunk_dump.sql.sort";
+               push @diff_commands, $sort_diff_command,
+               `$sort_diff_command > $diff_file_path.sort`;
+               push @diff_sort_file_paths, "$diff_file_path.sort";
+       }
+       
+       print join("\n", @diff_commands) . "\n\n";
+       
+       for my $diff_sort_file_path (@diff_sort_file_paths) {
+               print "\n$diff_sort_file_path\n";
+               print `cat $diff_sort_file_path`;
+       }
+       
+       return 1;
+}
+
+#/////////////////////////////////////////////////////////////////////////////
+
+=head2 parse_sql_file
+
+ Parameters  : $sql_file_path
+ Returns     : hash reference
+ Description : Parses the statements in an SQL file and generates a hash.
+
+=cut
+
+sub parse_sql_file {
+       my ($sql_file_path) = @_;
+       if (!$sql_file_path) {
+               notify($ERRORS{'WARNING'}, 0, "source VCL SQL file path 
argument was not supplied");
+               return;
+       }
+       
+       my $mn_os = get_mn_os();
+       
+       # Get the contents of the .sql file
+       my $sql_string = $mn_os->get_file_contents($sql_file_path);
+       if (!$sql_string) {
+               setup_print_error("Failed to retrieve contents of SQL file: 
$sql_file_path");
+               return;
+       }
+       
+       print "Parsing SQL file: $sql_file_path\n";
+       
+       # Remove comments
+       $sql_string =~ s/^--.*[\r\n]+//mg;
+       $sql_string =~ s/^#.*//mg;
+       
+       my $comment_regex = '
+               (
+                       /\*                             (?# Beginning of 
comment)
+                               (?:
+                                       [^*]            (?# Match anything 
other than a *)
+                                       |
+                                       [*][^/] (?# Match * only if not 
followed by a /)
+                               )*
+                       \*/[\s;]*               (?# End of comment)
+               )
+       ';
+       $sql_string =~ s/$comment_regex//sxg;
+       
+       my $sql_info = {};
+       
+       my @insert_statements;
+       $sql_info->{CREATE_TABLE} = {};
+       $sql_info->{ALTER_TABLE} = {};
+       $sql_info->{INSERT} = [];
+       
+       my @statements = split(/;\n/, $sql_string);
+       my $statement_count = scalar(@statements);
+       for (my $i=0; $i<$statement_count; $i++) {
+               my $statement = $statements[$i];
+               
+               # Collapse statement into a single line
+               $statement =~ s/\n/ /gs;
+               
+               # Remove any spaces from the beginning of the statement and 
consecutive spaces
+               $statement =~ s/(^|\s)\s+/$1/gs;
+               
+               if ($statement =~ /^CREATE TABLE/ ) {
+                       my $create_table = 
parse_create_table_statement($statement) || return;
+                       $sql_info->{CREATE_TABLE} = 
{%{$sql_info->{CREATE_TABLE}}, %$create_table};
+               }
+               elsif ($statement =~ /^ALTER TABLE/ ) {
+                       my $alter_table_info = 
parse_alter_table_statement($statement);
+                       if (!$alter_table_info) {
+                               setup_print_error("failed to parse ALTER TABLE 
statement:");
+                               print "$statement\n";
+                               return;
+                       }
+                       
+                       #setup_print_break();
+                       #print format_data($alter_table_info) . "\n\n";
+                       
+                       # Merge info with previously retrieved ALTER TABLE info
+                       for my $table_name (keys %$alter_table_info) {
+                               for my $statement_type (keys 
%{$alter_table_info->{$table_name}}) {
+                                       for my $key (keys 
%{$alter_table_info->{$table_name}{$statement_type}}) {
+                                               if 
(!defined($sql_info->{ALTER_TABLE}{$table_name}{$statement_type}{$key})) {
+                                                       
$sql_info->{ALTER_TABLE}{$table_name}{$statement_type}{$key} = 
$alter_table_info->{$table_name}{$statement_type}{$key};
+                                               }
+                                               else {
+                                                       setup_print_error("SQL 
file contains duplicate ALTER TABLE $statement_type $key statements");
+                                                       return;
+                                               }
+                                       }
+                               }
+                       }
+               }
+               elsif ($statement =~ /^INSERT/) {
+                       $statement =~ s/INSERT INTO/INSERT IGNORE INTO/;
+                       push @{$sql_info->{INSERT}}, $statement;
+               }
+               elsif ($statement =~ /^UPDATE/) {
+                       push @{$sql_info->{UPDATE}}, $statement;
+               }
+               elsif ($statement =~ /^DROP TABLE/) {
+                       my $table_name = parse_drop_table_statement($statement);
+                       push @{$sql_info->{DROP_TABLE}}, $table_name;
+               }
+               elsif ($statement =~ /^CREATE DATABASE/ ) {
+               }
+               elsif ($statement =~ /^SET/) {
+               }
+               else {
+                       setup_print_warning("SQL statement is not 
supported:\n$statement");
+                       return;
+               }
+       }
+       print "Done. (statement count: $statement_count)\n";
+       
+       for my $table_info ($sql_info->{CREATE_TABLE}, 
$sql_info->{ALTER_TABLE}) {
+               for my $table_name (keys %$table_info) {
+                       for my $constraint_name (keys 
%{$table_info->{$table_name}{CONSTRAINT}}) {
+                               my $constraint = 
$table_info->{$table_name}{CONSTRAINT}{$constraint_name};
+                               $constraint->{index_table} = $table_name;
+                               my $index_column = $constraint->{index_column};
+                               my $parent_table = $constraint->{parent_table};
+                               my $parent_column = 
$constraint->{parent_column};
+                               
+                               $sql_info->{CONSTRAINTS}{$constraint_name} = 
$constraint;
+                               push 
@{$sql_info->{REFERENCED_CONSTRAINTS}{$parent_table}{$parent_column}}, 
$constraint;
+                               
$sql_info->{REFERENCING_CONSTRAINTS}{$table_name}{$index_column} = $constraint;
+                       }
+                       
+                       for my $column_name (keys 
%{$table_info->{$table_name}{ADD}}) {
+                               my $column_info = 
$table_info->{$table_name}{ADD}{$column_name};
+                               
$sql_info->{ADD_COLUMN}{$table_name}{$column_name} = $column_info;
+                       }
+                       
+                       for my $column_name (keys 
%{$table_info->{$table_name}{DROP}}) {
+                               my $column_info = 
$table_info->{$table_name}{DROP}{$column_name};
+                               
$sql_info->{DROP_COLUMN}{$table_name}{$column_name} = $column_info;
+                               
+                               ###if 
(defined($sql_info->{ADD_COLUMN}{$table_name}{$column_name})) {
+                               ###     delete 
$sql_info->{ADD_COLUMN}{$table_name}{$column_name};
+                               ###}
+                       }
+               }
+       }
+       
+       if ($DEBUG) {
+               setup_print_break('=');
+               print "REFERENCED_CONSTRAINTS:\n\n";
+               for my $parent_table (sort { lc($a) cmp lc($b) } keys 
%{$sql_info->{REFERENCED_CONSTRAINTS}}) {
+                       for my $parent_column (sort { lc($a) cmp lc($b) } keys 
%{$sql_info->{REFERENCED_CONSTRAINTS}{$parent_table}}) {
+                               my @constraints = 
@{$sql_info->{REFERENCED_CONSTRAINTS}{$parent_table}{$parent_column}};
+                               
+                               for my $constraint (@constraints) {
+                                       my $index_table = 
$constraint->{index_table};
+                                       my $index_column = 
$constraint->{index_column};
+                                       print "$index_table.$index_column\n";
+                               }
+                               print "--> $parent_table.$parent_column\n\n";
+                       }
+               }
+               
+               setup_print_break('=');
+               print "REFERENCING_CONSTRAINTS:\n\n";
+               for my $index_table (sort { lc($a) cmp lc($b) } keys 
%{$sql_info->{REFERENCING_CONSTRAINTS}}) {
+                       for my $index_column (sort { lc($a) cmp lc($b) } keys 
%{$sql_info->{REFERENCING_CONSTRAINTS}{$index_table}}) {
+                               my $parent_table = 
$sql_info->{REFERENCING_CONSTRAINTS}{$index_table}{$index_column}{parent_table};
+                               my $parent_column = 
$sql_info->{REFERENCING_CONSTRAINTS}{$index_table}{$index_column}{parent_column};
+                               print "$index_table.$index_column --> 
$parent_table.$parent_column\n";
+                       }
+               }
+       }
+       
+       return $sql_info;
+}
+
+#/////////////////////////////////////////////////////////////////////////////
+
+=head2 parse_create_table_statement
+
+ Parameters  : 
+ Returns     : 
+ Description :
+
+=cut
+
+sub parse_create_table_statement {
+       my ($statement) = @_;
+       if (!$statement) {
+               notify($ERRORS{'WARNING'}, 0, "SQL statement argument was not 
supplied");
+               return;
+       }
+       
+       my $table_info = {};
+       
+       my ($table_name, $table_definition, $table_options) = $statement =~ 
/CREATE TABLE(?: IF NOT EXISTS)? `?([\w_\$]+)`? \(\s*(.+)\s*\)\s*(.*)\s*$/g;
+       if (!$table_name) {
+               setup_print_error("failed to determine table 
name:\n\n$statement\n\n" . string_to_ascii($statement) . "");
+               return;
+       }
+       elsif (!$table_definition) {
+               setup_print_error("failed to determine table 
definition:\n\n$statement\n\n" . string_to_ascii($statement) . "");
+               return;
+       }
+       $table_options = '' if !defined($table_options);
+       
+       $table_info->{$table_name}{STATEMENT} = $statement;
+       
+       #..........
+       
+       # Extract the CONSTRAINT definitions
+       my $constraint_regex = '
+               \s*                                     (?# omit leading spaces)
+               (
+                       CONSTRAINT              (?# must contain KEY)
+                       [^,]+                           (?# any character 
except commas)
+               )
+               [,\s]*                          (?# omit trailing comma and 
spaces)
+       ';
+       my @constraint_definitions = $table_definition =~ /$constraint_regex/gx;
+       my $constraint_definition_count = scalar(@constraint_definitions) || 0;
+       
+       for (my $i=0; $i<$constraint_definition_count; $i++) {
+               my $constraint_definition = $constraint_definitions[$i];
+               my $constraint_info = 
parse_constraint_definition($constraint_definition);
+               my $constraint_name = $constraint_info->{name};
+               $table_info->{$table_name}{CONSTRAINT}{$constraint_name} = 
$constraint_info;
+       }
+       
+       # Remove the CONSTRAINT definitions
+       $table_definition =~ s/$constraint_regex//gx;
+       
+       #..........
+       
+       # Extract the KEY definitions
+       my $key_regex = '
+               \s*                                     (?# omit leading spaces)
+               (
+                       [\w\s]*                 (?# words preceding key such as 
PRIMARY, UNIQUE, etc)
+                       KEY                             (?# must contain KEY)
+                       [^\)]+                  (?# any character except 
closing parenthesis, all key definitions of a set of parenthesis)
+                       \)                                      (?# closing 
parenthesis)
+                       (?:
+                               [^,]*[^,\s]     (?# index options may exist 
after the closing parenthesis, make sure this ends with a non-space character)
+                       )?
+               )
+               [,\s]*                          (?# omit trailing comma and 
spaces)
+       ';
+       my @key_definitions = $table_definition =~ /$key_regex/gx;
+       my $key_definition_count = scalar(@key_definitions);
+       
+       for (my $i=0; $i<$key_definition_count; $i++) {
+               my $key_definition = $key_definitions[$i];
+               
+               my $key_definition_regex = '
+                       (
+                               \w*                     (?# key type: PRIMARY, 
UNIQUE)
+                       )?
+                       \s?
+                       KEY
+                       \s
+                       `?
+                       (
+                               [\w_\$]+                (?# key name)
+                       )?                                      (?# key name is 
not set for primary keys)
+                       `?
+                       \s*
+                       \(                                      (?# opening 
parenthesis)
+                               ([^\)]+)
+                       \)                                      (?# closing 
parenthesis)
+                       [,\s]*                  (?# omit trailing comma and 
spaces)
+               ';
+               
+               my ($key_type, $key_name, $column_list) = $key_definition =~ 
/$key_definition_regex/x;
+               if (!defined($key_type) && !defined($key_name)) {
+                       setup_print_error("failed to determine key type or 
name:\n\n$key_definition\n\n" . string_to_ascii($key_definition) . "");
+                       return;
+               }
+               elsif (!defined($column_list)) {
+                       setup_print_error("failed to determine column 
list:\n\n$key_definition\n\n" . string_to_ascii($key_definition) . "");
+                       return;
+               }
+               $key_type = 'INDEX' if (!$key_type);
+               $key_name = 'PRIMARY' if !($key_name);
+               
+               $column_list =~ s/[`\s]//g;
+               my @columns = split(/,/, $column_list);
+               
+               my $key_info = {};
+               $key_info->{STATEMENT} = $key_definition;
+               $key_info->{TYPE} = $key_type;
+               $key_info->{name} = $key_name;
+               %{$key_info->{COLUMNS}} = map { $_ => 1 } @columns;
+               
+               $table_info->{$table_name}{INDEXES}{$key_name} = $key_info;
+       }
+       
+       # Error check, make sure number of times 'KEY' appears in original 
statement matches number of keys found
+       my @statement_keys = $statement =~ /KEY/g;
+       my $statement_key_count = scalar(@statement_keys);
+       if ($statement_key_count ne ($key_definition_count + 
$constraint_definition_count)) {
+               setup_print_error("statement KEY count ($statement_key_count) 
does not match the number of keys parsed ($key_definition_count) + constraints 
($constraint_definition_count)");
+               return;
+       }
+       
+       # Remove the KEY definitions
+       $table_definition =~ s/$key_regex//gx;
+       
+       #..........
+       
+       # Retrieve the column definitions
+       my $column_regex = '
+               \s*                                     (?# omit leading spaces)
+               (
+                       (?:
+                               [^,\(]+         (?# any character execept for 
comma and opening parenthesis)
+                               |                               (?# -or-)
+                               \(                              (?# opening 
parenthesis)
+                                       [^\)]+  (?# any character execept for 
closing parenthesis)
+                               \)                              (?# closing 
parenthesis)
+                       )+                                      (?# match 
either case multiple times because normal charcters can come after the closing 
parenthesis)
+               )
+               [,\s]*                          (?# omit trailing comma and 
spaces)
+       ';
+       my @column_definitions = $table_definition =~ /$column_regex/gx;
+       
+       for (my $i=0; $i<scalar(@column_definitions); $i++) {
+               my $column_definition = $column_definitions[$i];
+               
+               my $column_info = parse_column_definition($column_definition);
+               if (!$column_info) {
+                       setup_print_error("failed to parse $table_name 
definition, column definition could not be parsed:\n$column_definition");
+                       return;
+               }
+               
+               my $column_name = $column_info->{name};
+               $table_info->{$table_name}{COLUMNS}{$column_name} = 
$column_info;
+               
+               push @{$table_info->{$table_name}{COLUMN_ORDER}}, 
$table_info->{$table_name}{COLUMNS}{$column_name};
+       }
+       
+       #..........
+       
+       # Parse the table options
+       my $table_options_patterns = {
+               'AUTO_INCREMENT' => 'AUTO_INCREMENT',
+               '(?:DEFAULT\s*)?(?:CHARSET|CHARACTER SET)' => 'CHARSET',
+               'CHECKSUM' => 'CHECKSUM',
+               'COMMENT' => 'COMMENT',
+               'CONNECTION' => 'CONNECTION',
+               '(?:DEFAULT\s*)?COLLATE' => 'COLLATE',
+               'DATA DIRECTORY' => 'DATA_DIRECTORY',
+               'DELAY_KEY_WRITE' => 'DELAY_KEY_WRITE',
+               '(ENGINE|TYPE)' => 'ENGINE',
+               'INDEX DIRECTORY' => 'INDEX_DIRECTORY',
+               'INSERT_METHOD' => 'INSERT_METHOD',
+               'KEY_BLOCK_SIZE' => 'KEY_BLOCK_SIZE',
+               'MAX_ROWS' => 'MAX_ROWS',
+               'MIN_ROWS' => 'MIN_ROWS',
+               'PACK_KEYS' => 'PACK_KEYS',
+               'PASSWORD' => 'PASSWORD',
+               'RAID_TYPE' => 'RAID_TYPE',
+               'RAID_CHUNKS' => 'RAID_CHUNKS',
+               'RAID_CHUNKSIZE' => 'RAID_CHUNKSIZE',
+               'ROW_FORMAT' => 'ROW_FORMAT',
+               'TABLESPACE' => 'TABLESPACE',
+               'UNION' => 'UNION',
+       };
+       
+       for my $table_option_pattern (keys %$table_options_patterns) {
+               my $synonym = $table_options_patterns->{$table_option_pattern};
+               my $table_option_regex = $table_option_pattern . 
'\s*=\s*(\'[^\']+\'|[^\s]+)\s*';
+               my ($value) = $table_options =~ /$table_option_regex/gx;
+               if ($value) {
+                       $value =~ s/(^'|'$)//g;
+                       $table_options =~ s/$table_option_regex//gx;
+                       $table_info->{$table_name}{OPTIONS}{$synonym} = $value;
+               }
+       }
+       if ($table_options =~ /\S/) {
+               print "WARNING: $table_name table options not recognized: 
'$table_options'\n";
+       }
+       
+       #print "\n" . format_data($table_info) . "\n";
+       return $table_info;
+}
+
+#/////////////////////////////////////////////////////////////////////////////
+
+=head2 parse_column_definition
+
+ Parameters  : $table_name, $column_definition
+ Returns     : 
+ Description :
+
+=cut
+
+sub parse_column_definition {
+       my ($column_definition) = @_;
+       if (!$column_definition) {
+               notify($ERRORS{'WARNING'}, 0, "column definition argument was 
not provided");
+               return;
+       }
+       
+       my $column_definition_regex = '
+               ^
+                       `?
+                               (                                       (?# 
begin column name match)
+                                       [\w_\$]+                (?# column name)
+                               )                                       (?# end 
column name match)
+                       `?
+                       \s+                                     (?# space after 
column name)
+                       (                                               (?# 
begin data type match)
+                               (?:
+                                       \w[^\s\(]+      (?# beginning of data 
type, must start with a letter, continue until space or opening parenthesis if 
found)
+                                       |
+                                       \(                              (?# 
opening parenthesis)
+                                               [^\)]+  (?# any character 
execept for closing parenthesis)
+                                       \)                              (?# 
closing parenthesis)
+                               )+
+                       )                                               (?# end 
data type match)
+                       \s*
+                       (
+                               .*                                      (?# 
column options)
+                       )
+       ';
+       
+       my ($column_name, $data_type, $column_options) = $column_definition =~ 
/$column_definition_regex/x;
+       if (!defined($column_name)) {
+               setup_print_error("failed to determine column name from column 
definition:\n$column_definition\n");
+               return;
+       }
+       elsif (!defined($data_type)) {
+               setup_print_error("failed to determine data type from column 
definition:\n$column_definition");
+               return;
+       }
+       $column_options = '' if !defined($column_options);
+       
+       my $column_info = {
+               'name' => $column_name,
+               'STATEMENT' => $column_definition,
+               'DATA_TYPE' => $data_type,
+               'OPTIONS' => {},
+       };
+       
+       if ($column_options =~ s/unsigned//i) {
+               $column_info->{OPTIONS}{unsigned} = 1;
+       }
+       
+       if ($column_options =~ s/AUTO_INCREMENT//i) {
+               $column_info->{OPTIONS}{AUTO_INCREMENT} = 1;
+       }
+       
+       my ($comment) = $column_options =~ /COMMENT '([^']+)'/i;
+       if (defined($comment)) {
+               $column_info->{OPTIONS}{COMMENT} = $comment;
+       }
+       $column_options =~ s/COMMENT '([^']+)'//i;
+       
+       # `column` varchar(xx) NOT NULL DEFAULT 'xxx',
+       # `column` varchar(xx) DEFAULT NULL,
+       # `column` varchar(xx) NULL default NULL,
+       # `column` varchar(xx) NULL default 'xxx',a
+       
+       my ($default) = $column_options =~ /DEFAULT '?(NULL|[^']*)'?/i;
+       if (defined($default)) {
+               $column_info->{OPTIONS}{DEFAULT} = $default;
+       }
+       else {
+               $default = '';
+       }
+       $column_options =~ s/DEFAULT '?(NULL|[^']*)'?//i;
+       
+       my $not_null = $column_options =~ s/NOT NULL//i;
+       if ($not_null) {
+               $column_info->{OPTIONS}{'NOT_NULL'} = 1;
+       }
+       else {
+               $column_info->{OPTIONS}{'NOT_NULL'} = 0;
+       }
+       
+       # Check if column does not have NOT NULL set and no default value:
+       #   `column` text
+       if (!$not_null && $default eq '') {
+               $default = 'NULL';
+               $column_info->{OPTIONS}{DEFAULT} = $default;
+       }
+       
+       # Remove 'NULL' from the column options, it won't get removed from the 
above statements for this case:
+       #    `column` varchar(xx) NULL default NULL
+       if (!$not_null || $default =~ /null/i) {
+               $column_options =~ s/NULL\s*//i;
+       }
+       
+       if ($column_options =~ /[^\ ]/) {
+               setup_print_warning("$column_name column options not 
recognized: '$column_options'");
+       }
+       
+       return $column_info;
+}
+
+#/////////////////////////////////////////////////////////////////////////////
+
+=head2 parse_alter_table_statement
+
+ Parameters  : 
+ Returns     : 
+ Description :
+
+=cut
+
+sub parse_alter_table_statement {
+       my ($statement) = @_;
+       if (!$statement) {
+               notify($ERRORS{'WARNING'}, 0, "SQL statement argument was not 
supplied");
+               return;
+       }
+       
+       my ($table_name, $table_definition) = $statement =~ /ALTER TABLE 
`?([\w_\$]+)`?\s*(.+)\s*$/g;
+       if (!$table_name) {
+               setup_print_error("failed to determine table 
name:\n\n$statement\n\n" . string_to_ascii($statement) . "");
+               return;
+       }
+       elsif (!$table_definition) {
+               setup_print_error("failed to determine table 
definition:\n\n$statement\n\n" . string_to_ascii($statement) . "");
+               return;
+       }
+       
+       my $alter_table_info = {};
+       
+       #..........
+       
+       # Extract the CONSTRAINT definitions
+       my $constraint_regex = '
+               \s*                                             (?# omit 
leading spaces)
+               (
+                       ADD\sCONSTRAINT
+                       [^,]+                                   (?# any 
character except commas)
+               )
+               [,\s]*                                  (?# omit trailing comma 
and spaces)
+       ';
+       my @constraint_definitions = $table_definition =~ /$constraint_regex/gx;
+       my $constraint_definition_count = scalar(@constraint_definitions) || 0;
+       
+       for (my $i=0; $i<$constraint_definition_count; $i++) {
+               my $constraint_definition = $constraint_definitions[$i];
+               my $constraint_info = 
parse_constraint_definition($constraint_definition);
+               my $constraint_name = $constraint_info->{name};
+               
$alter_table_info->{$table_name}{'CONSTRAINT'}{$constraint_name} = 
$constraint_info;
+       }
+       
+       # Remove the CONSTRAINT definitions
+       $table_definition =~ s/$constraint_regex//gx;
+       
+       #..........
+       
+       # ADD `<column name>` bit(1) NULL default NULL
+       # Extract the ADD definitions
+       my $add_regex = '
+               \s*                                     (?# omit leading spaces)
+               ADD\s
+               (
+                       [^,]+          (?# any character except commas)
+               )
+               [,\s]*                          (?# omit trailing comma and 
spaces)
+       ';
+       
+       my @add_definitions = $table_definition =~ /$add_regex/gx;
+       my $add_definition_count = scalar(@add_definitions) || 0;
+       
+       for (my $i=0; $i<$add_definition_count; $i++) {
+               my $add_definition = $add_definitions[$i];
+               my $column_info = parse_column_definition($add_definition);
+               if (!$column_info) {
+                       setup_print_error("failed to parse alter table 
statement:\n$statement\nADD definition:\n$add_definition");
+                       return;
+               }
+               
+               my $column_name = $column_info->{name};
+               $alter_table_info->{$table_name}{ADD}{$column_name} = 
$column_info;
+       }
+       
+       $table_definition =~ s/$add_regex//gx;
+       
+       #..........
+       
+       # Extract the DROP definitions
+       my $drop_regex = '
+               \s*                                     (?# omit leading spaces)
+               DROP\s
+               `?
+               (
+                       [\w_\$]+          (?# any character except commas)
+               )
+               `?
+               [,\s]*                          (?# omit trailing comma and 
spaces)
+       ';
+       
+       my @drop_column_names = $table_definition =~ /$drop_regex/gx;
+       my $drop_column_count = scalar(@drop_column_names) || 0;
+       
+       for (my $i=0; $i<$drop_column_count; $i++) {
+               my $drop_column_name = $drop_column_names[$i];
+               $alter_table_info->{$table_name}{DROP}{$drop_column_name} = 1;
+       }
+       
+       $table_definition =~ s/$drop_regex//gx;
+       
+       #..........
+       
+       if ($table_definition =~ /\S/) {
+               setup_print_warning("part of alter $table_name table definition 
was not handled:");
+               print "table definition:\n$table_definition\n\n";
+               print "statement:\n$statement\n\n";
+               return;
+       }
+       
+       return $alter_table_info;
+}
+
+#/////////////////////////////////////////////////////////////////////////////
+
+=head2 parse_drop_table_statement
+
+ Parameters  : 
+ Returns     : 
+ Description :
+
+=cut
+
+sub parse_drop_table_statement {
+       my ($statement) = @_;
+       if (!$statement) {
+               notify($ERRORS{'WARNING'}, 0, "SQL statement argument was not 
supplied");
+               return;
+       }
+       
+       # DROP TABLE IF EXISTS `<table name>`;
+       my $drop_table_regex = '
+               DROP\sTABLE\s
+               (?:IF\sEXISTS\s)?
+               `?
+                       ([\w_\$]+)                                              
        (?# table name)
+               `?
+       ';
+       
+       my ($table_name) = $statement =~ /$drop_table_regex/gx;
+       if ($table_name) {
+               return $table_name;
+       }
+       else {
+               setup_print_error("failed to determine table name from 
statement: $statement");
+               return;
+       }
+}
+
+#/////////////////////////////////////////////////////////////////////////////
+
+=head2 parse_insert_statements
+
+ Parameters  : 
+ Returns     : 
+ Description :
+
+=cut
+
+sub parse_insert_statements {
+       my @statements = @_;
+       if (!@statements) {
+               notify($ERRORS{'WARNING'}, 0, "SQL statement argument was not 
supplied");
+               return;
+       }
+       
+       # INSERT IGNORE INTO `usergroupprivtype` (`id`, `name`, `help`) VALUES 
(1, 'xxx', 'yyy'),
+       my $insert_regex = '
+               INSERT\s
+               (?:IGNORE\s)?
+               (?:INTO\s)?
+               `?
+                       ([\w_\$]+)                                              
        (?# table name)
+               `?\s
+       ';
+       
+       my $insert_info = {};
+       for my $statement (@statements) {
+               my ($table_name) = $statement =~ /$insert_regex/gx;
+               if (!$table_name) {
+                       setup_print_error("failed to determine table 
name:\n\n$statement");
+                       return;
+               }
+               
+               if (!defined($insert_info->{$table_name})) {
+                       $insert_info->{$table_name} = [];
+               }
+               push @{$insert_info->{$table_name}}, $statement;
+       }
+       
+       return $insert_info;
+}
+
+#/////////////////////////////////////////////////////////////////////////////
+
+=head2 parse_update_statement
+
+ Parameters  : 
+ Returns     : 
+ Description :
+
+=cut
+
+sub parse_update_statement {
+       my ($statement) = @_;
+       if (!$statement) {
+               notify($ERRORS{'WARNING'}, 0, "SQL statement argument was not 
supplied");
+               return;
+       }
+       
+       # UPDATE <table_name> SET <column_name> = <value> WHERE ...;
+       my $insert_regex = '
+               UPDATE\s
+               (?:IGNORE\s)?
+               `?
+                       ([\w_\$]+)                                              
        (?# table name)
+               `?\s
+               SET\s
+               `?
+                  ([\w_\$]+)                                                   
(?# column name)
+               `?
+               \s=\s
+                  ([\S]+)                                                      
(?# value)
+       ';
+       
+       my ($table_name, $column_name, $value) = $statement =~ 
/$insert_regex/gx;
+       if (!defined($table_name)) {
+               setup_print_error("failed to determine table name from 
statement:\n$statement");
+               return;
+       }
+       
+       my $update_info = {
+               'table' => $table_name,
+               'column' => $column_name,
+               'value' => $value,
+               'STATEMENT' => $statement,
+       };
+       
+       return $update_info;
+}
+
+#/////////////////////////////////////////////////////////////////////////////
+
+=head2 parse_constraint_definition
+
+ Parameters  : 
+ Returns     : 
+ Description :
+
+=cut
+
+sub parse_constraint_definition {
+       my ($constraint_definition) = @_;
+       if (!$constraint_definition) {
+               notify($ERRORS{'WARNING'}, 0, "constraint definition argument 
was not supplied");
+               return;
+       }
+       
+       # Remove text before "CONSTRAINT" for consistency
+       $constraint_definition =~ s/.*(CONSTRAINT)/$1/;
+       
+       # CONSTRAINT `constraint_name` FOREIGN KEY (`this_column`) REFERENCES 
`that_table` (`that_column`) ON DELETE SET NULL ON UPDATE CASCADE
+       my $constraint_definition_regex = '
+               CONSTRAINT\s
+               `?([\w_\$]+)`?\s                (?# constraint name)
+               FOREIGN\sKEY\s
+               \(                                                      (?# 
opening parenthesis)
+                       `?([^\)`]+)`?           (?# column name for this table)
+               \)\s                                            (?# closing 
parenthesis)
+               REFERENCES\s
+               `?([^\)`\s]+)`?\s               (?# other table)
+               \(                                                      (?# 
opening parenthesis)
+               `?([^\)`\s]+)`?         (?# column name for other table)
+               \)\s?                                           (?# closing 
parenthesis)
+               (?:
+                       ([^,]*[^,\s])?          (?# options)
+               )?
+       ';
+       
+       my ($constraint_name, $index_column_name, $parent_table_name, 
$parent_column_name, $constraint_options) = $constraint_definition =~ 
/$constraint_definition_regex/gx;
+       if (!defined($constraint_name)) {
+               setup_print_error("failed to parse constraint name");
+               return;
+       }
+       
+       #print "name: $constraint_name\n";
+       #print "index column: $index_column_name\n";
+       #print "parent table: $parent_table_name\n";
+       #print "parent column: $parent_column_name\n";
+       
+       my $constraint_info = {};
+       $constraint_info->{name} = $constraint_name;
+       $constraint_info->{index_column} = $index_column_name;
+       $constraint_info->{parent_table} = $parent_table_name;
+       $constraint_info->{parent_column} = $parent_column_name;
+       $constraint_info->{STATEMENT} = $constraint_definition;
+       
+       if ($constraint_options) {
+               #print "constraint options: '$constraint_options'\n";
+               
+               my $on_update_regex = 'ON UPDATE ((?:SET|NO)?\s?[\w]+)';
+               my $on_delete_regex = 'ON DELETE ((?:SET|NO)?\s?[\w]+)';
+               my ($on_update_value) = $constraint_options =~ 
/$on_update_regex/ig;
+               my ($on_delete_value) = $constraint_options =~ 
/$on_delete_regex/ig;
+               
+               if ($on_update_value) {
+                       #print "ON UPDATE: '$on_update_value'\n" if 
$on_update_value;
+                       $constraint_info->{OPTIONS}{ON_UPDATE} = 
$on_update_value;
+               }
+               
+               if ($on_delete_value) {
+                       #print "ON DELETE: '$on_delete_value'\n" if 
$on_delete_value;
+                       $constraint_info->{OPTIONS}{ON_DELETE} = 
$on_delete_value;
+               }
+               
+               # Check for remaining constraint options
+               $constraint_options =~ s/$on_update_regex//ig;
+               $constraint_options =~ s/$on_delete_regex//ig;
+               if ($constraint_options =~ /\w/) {
+                       print "WARNING: $index_column_name --> 
$parent_table_name.$parent_column_name constraint options not recognized: 
'$constraint_options'\n";
+               }
+       }
+       
+       #print "constraint info:\n" . format_data($constraint_info) . "\n";
+       return $constraint_info;
+}
+
+#/////////////////////////////////////////////////////////////////////////////
+
+=head2 compare_database_to_reference
+
+ Parameters  : 
+ Returns     : 
+ Description :
+
+=cut
+
+sub compare_database_to_reference {
+       my ($database_info, $reference_info) = @_;
+       if (!$database_info || !$reference_info) {
+               notify($ERRORS{'WARNING'}, 0, "database and reference schema 
arguments were not supplied");
+               return;
+       }
+       
+       print "comparing schemas\n\n";
+       
+       my $changes = {
+               ADD_COLUMN => [],
+               ADD_INDEX => [],
+               ADD_FOREIGN_KEY => [],
+               ALTER_INDEX => [],
+               CHANGE_COLUMN => [],
+               CREATE_TABLE => [],
+               DROP_COLUMN => [],
+               DROP_FOREIGN_KEY => [],
+               DROP_TABLE => [],
+               INSERT => [],
+       };
+       
+       for my $table_name (keys %$RENAME_COLUMNS) {
+               for my $new_column_name (keys 
%{$RENAME_COLUMNS->{$table_name}}) {
+                       my $original_column_name = 
$RENAME_COLUMNS->{$table_name}{$new_column_name};
+                       if 
(!defined($database_info->{CREATE_TABLE}{$table_name}{COLUMNS}{$original_column_name}))
 {
+                               print "$table_name.$original_column_name won't 
be renamed to $new_column_name because $original_column_name column does not 
exist\n" if $DEBUG;
+                               next;
+                       }
+                       elsif 
(defined($database_info->{CREATE_TABLE}{$table_name}{COLUMNS}{$new_column_name}))
 {
+                               
setup_print_warning("$table_name.$original_column_name won't be renamed to 
$new_column_name because $new_column_name column already exists");
+                               next;
+                       }
+                       
+                       my $reference_column = 
$reference_info->{CREATE_TABLE}{$table_name}{COLUMNS}{$new_column_name};
+                       if (!$reference_column) {
+                               setup_print_error("$table_name.$new_column_name 
definition does not exist in vcl.sql");
+                               print 
format_data($reference_info->{CREATE_TABLE}{$table_name}{COLUMNS});
+                               next;
+                       }
+                       
+                       print "column will be renamed: 
$table_name.$original_column_name --> $new_column_name\n";
+                       push @{$changes->{CHANGE_COLUMN}}, "ALTER TABLE 
`$table_name` CHANGE `$original_column_name` $reference_column->{STATEMENT};";
+               }
+       }
+       
+       my $drop_indexes = {};
+       REFERENCE_TABLE: for my $table_name (sort { lc($a) cmp lc($b) } keys 
%{$reference_info->{CREATE_TABLE}}) {
+               my $reference_table = 
$reference_info->{CREATE_TABLE}{$table_name};
+               #if ($DEBUG) {
+               #       print format_data($reference_table) . "\n";
+               #       setup_print_break('.');
+               #}
+               
+               my $database_table = 
$database_info->{CREATE_TABLE}{$table_name};
+               if (!defined($database_table)) {
+                       print "table exists in reference but not database: 
$table_name\n" if $DEBUG;
+                       push @{$changes->{CREATE_TABLE}}, 
$reference_table->{STATEMENT};
+                       #$changes->{CREATE_TABLE}{$table_name} = 
$reference_table;
+               }
+               else {
+                       my $reference_columns = $reference_table->{COLUMNS};
+                       my $database_columns = $database_table->{COLUMNS};
+                       
+                       REFERENCE_COLUMN: for my $column_name (sort { lc($a) 
cmp lc($b) } keys %$reference_columns) {
+                               my $reference_column = 
$reference_columns->{$column_name};
+                               my $database_column = 
$database_columns->{$column_name};
+                               if (!defined($database_column)) {
+                                       if (my $original_column_name = 
$RENAME_COLUMNS->{$table_name}{$column_name}) {
+                                               if 
($database_columns->{$original_column_name}) {
+                                                       print "column will not 
be added because it is being renamed: $table_name.$original_column_name --> 
$column_name\n" if $DEBUG;
+                                                       next REFERENCE_COLUMN;
+                                               }
+                                       }
+                                       
+                                       print "column exists in reference but 
not database: $table_name.$column_name\n" if $DEBUG;
+                                       push @{$changes->{ADD_COLUMN}}, "ALTER 
TABLE `$table_name` ADD $reference_column->{STATEMENT};";
+                                       
#$changes->{ADD_COLUMN}{$table_name}{$column_name} = $reference_column;
+                                       
+                                       next REFERENCE_COLUMN;
+                               }
+                               
+                               my $reference_data_type = 
$reference_column->{DATA_TYPE};
+                               my $database_data_type = 
$database_column->{DATA_TYPE};
+                               if (lc($reference_data_type) ne 
lc($database_data_type)) {
+                                       print "$table_name.$column_name data 
type will be changed: $database_data_type --> $reference_data_type\n";
+                                       push @{$changes->{CHANGE_COLUMN}}, 
"ALTER TABLE `$table_name` CHANGE `$column_name` 
$reference_column->{STATEMENT};";
+                               }
+                               
+                               REFERENCE_COLUMN_OPTION: for my $option_name 
(sort { lc($a) cmp lc($b) } keys %{$reference_column->{OPTIONS}}) {
+                                       my $reference_column_value = 
$reference_column->{OPTIONS}{$option_name};
+                                       my $database_column_value = 
$database_column->{OPTIONS}{$option_name};
+                                       
+                                       if (!defined($database_column_value)) {
+                                               print "$table_name.$column_name 
'$option_name' is set to '$reference_column_value' in reference, undefined in 
database\n" if $DEBUG;
+                                               print "reference : 
$reference_column->{STATEMENT}\n";
+                                               print "database  : 
$database_column->{STATEMENT}\n";
+                                       }
+                                       elsif (lc($reference_column_value) ne 
lc($database_column_value)) {
+                                               print "$table_name.$column_name 
'$option_name' different, reference: '$reference_column_value', database: 
'$database_column_value'\n" if $DEBUG;
+                                       }
+                                       else {
+                                               next REFERENCE_COLUMN_OPTION;
+                                       }
+                                       
+                                       push @{$changes->{CHANGE_COLUMN}}, 
"ALTER TABLE `$table_name` CHANGE `$column_name` 
$reference_column->{STATEMENT};";
+                                       
#$changes->{CHANGE_COLUMN}{$table_name}{$column_name} = $reference_column;
+                               }
+                       }
+                       
+                       my $reference_table_indexes = 
$reference_table->{INDEXES};
+                       #print format_data($reference_table_indexes) . "\n";
+                       
+                       my $database_table_indexes = $database_table->{INDEXES};
+                       REFERENCE_TABLE_INDEX: for my $reference_index_name 
(keys %$reference_table_indexes) {
+                               my $reference_index = 
$reference_table_indexes->{$reference_index_name};
+                               my $reference_index_type = 
$reference_index->{TYPE};
+                               my @reference_index_column_names = sort { 
lc($a) cmp lc($b) } keys %{$reference_index->{COLUMNS}};
+                               my $reference_index_statement = 
$reference_index->{STATEMENT};
+                               
+                               # Check if database table contains an index 
with the same name
+                               if 
(!defined($database_table_indexes->{$reference_index_name})) {
+                                       print "$table_name table 
'$reference_index_name' index does not exist in database\n" if $DEBUG;
+                                       push @{$changes->{ADD_INDEX}}, "ALTER 
TABLE `$table_name` ADD $reference_index_statement;";
+                                       
#$changes->{ADD_INDEX}{$table_name}{$reference_index_name} = $reference_index;
+                                       next REFERENCE_TABLE_INDEX;
+                               }
+                               else {
+                                       #print "$table_name table 
'$reference_index_name' index exists in database\n" if $DEBUG;
+                               }
+                               
+                               # Index with same name exists, compare them
+                               my $database_table_index = 
$database_table_indexes->{$reference_index_name};
+                               my $database_table_index_type = 
$database_table_index->{TYPE};
+                               my @compare_table_index_column_names = sort { 
lc($a) cmp lc($b) } keys %{$database_table_index->{COLUMNS}};
+                               my $database_table_index_statement = 
$database_table_index->{STATEMENT};
+                               
+                               
+                               my $different = 0;
+                               if ($reference_index_type ne 
$database_table_index_type) {
+                                       $different = 1;
+                                       if ($DEBUG) {
+                                               print "$table_name table 
'$reference_index_name' index type is different\n";
+                                               print "reference : 
$reference_index_type\n";
+                                               print "database  : 
$database_table_index_type\n";
+                                       }
+                               }
+                               elsif 
(!compare_array_elements(\@reference_index_column_names, 
\@compare_table_index_column_names)) {
+                                       $different = 1;
+                                       if ($DEBUG) {
+                                               print "$table_name table 
'$reference_index_name' index contains different columns:\n";
+                                               print "reference : " . join(', 
', @reference_index_column_names) . "\n";
+                                               print "database  : " . join(', 
', @compare_table_index_column_names) . "\n";
+                                       }
+                               }
+                               
+                               if ($different) {
+                                       $drop_indexes->{$reference_index_name} 
= 1;
+                                       push @{$changes->{ALTER_INDEX}}, "ALTER 
TABLE `$table_name` DROP INDEX `$reference_index_name` , ADD 
$reference_index_statement;";
+                                       
#$changes->{ALTER_INDEX}{$table_name}{$reference_index_name} = $reference_index;
+                               }
+                       } # reference table index
+               }  # database table defined
+               
+               my @column_order = @{$reference_table->{COLUMN_ORDER}};
+               for (my $i=1; $i<scalar(@column_order); $i++) {
+                       my $column = $column_order[$i];
+                       my $previous_column = $column_order[$i-1];
+                       push @{$changes->{MODIFY_COLUMN}}, "ALTER TABLE 
`$table_name` MODIFY COLUMN $column->{STATEMENT} AFTER 
`$previous_column->{name}`;";
+               }
+               
+       } # reference table
+       
+       # Check for explicit "DROP TABLE" statements
+       for my $table_name (sort { lc($a) cmp lc($b) } 
@{$reference_info->{DROP_TABLE}}) {
+               if (defined($database_info->{CREATE_TABLE}{$table_name})) {
+                       push @{$changes->{DROP_TABLE}}, "DROP TABLE IF EXISTS 
`$table_name`;";
+               }
+       }
+       
+       my @insert_statements = @{$reference_info->{INSERT}};
+       for my $insert_statement (@insert_statements) {
+               push @{$changes->{INSERT}}, $insert_statement;
+       }
+       
+       ## Check for explicit "ADD COLUMN" statements
+       #my $add_columns = {};
+       #for my $table_name (sort { lc($a) cmp lc($b) } keys 
%{$reference_info->{ADD_COLUMN}}) {
+       #       for my $column_name (sort { lc($a) cmp lc($b) } keys 
%{$reference_info->{ADD_COLUMN}{$table_name}}) {
+       #               if 
($database_info->{CREATE_TABLE}{$table_name}{COLUMNS}{$column_name}) {
+       #                       print "$table_name.$column_name already exists 
in database\n";
+       #                       next;
+       #               }
+       #               else {
+       #                       print "$table_name.$column_name will be 
added\n" if $DEBUG;
+       #                       my $reference_column = 
$reference_info->{ADD_COLUMN}{$table_name}{$column_name};
+       #                       push @{$changes->{ADD_COLUMN}}, "ALTER TABLE 
`$table_name` ADD $reference_column->{STATEMENT}";
+       #                       $add_columns->{$table_name}{$column_name} = 1;
+       #               }
+       #       }
+       #}
+       
+       
+       
+       # Check for explicit "DROP COLUMN" statements
+       my $drop_columns = {};
+       for my $table_name (sort { lc($a) cmp lc($b) } keys 
%{$reference_info->{DROP_COLUMN}}) {
+               for my $column_name (sort { lc($a) cmp lc($b) } keys 
%{$reference_info->{DROP_COLUMN}{$table_name}}) {
+                       if 
(defined($database_info->{CREATE_TABLE}{$table_name}{COLUMNS}{$column_name})) {
+                               print "column exists in database, it will be 
dropped: $table_name.$column_name\n" if $DEBUG;
+                       }
+                       else {
+                               print "column does not exist in database and 
won't be added, it won't be dropped: $table_name.$column_name\n" if $DEBUG;
+                               next;
+                       }
+                       
+                       push @{$changes->{DROP_COLUMN}}, "ALTER TABLE 
`$table_name` DROP `$column_name`";               
+                       $drop_columns->{$table_name}{$column_name} = 1;
+                       
+                       my $referenced_constraints = 
$database_info->{REFERENCED_CONSTRAINTS}{$table_name}{$column_name};
+                       if ($referenced_constraints) {
+                               #print "referenced constraints:\n" . 
format_data($referenced_constraints) . "\n";
+                       }
+                       
+                       my $referencing_constraint = 
$database_info->{REFERENCING_CONSTRAINTS}{$table_name}{$column_name};
+                       if ($referencing_constraint) {
+                               #print "referencing constraints:\n" . 
format_data($referencing_constraint) . "\n";
+                               push @{$changes->{DROP_FOREIGN_KEY}}, "ALTER 
TABLE `$table_name` DROP FOREIGN KEY `$referencing_constraint->{name}`;";
+                       }
+                       
+                       my $database_table_indexes = 
$database_info->{CREATE_TABLE}{$table_name}{INDEXES};
+                       for my $database_index_name (keys 
%$database_table_indexes) {
+                               my $database_index = 
$database_table_indexes->{$database_index_name};
+                               my @database_index_column_names = sort { lc($a) 
cmp lc($b) } keys %{$database_index->{COLUMNS}};
+                               if (grep { $_ eq $column_name } 
@database_index_column_names) {
+                                       if 
(scalar(@database_index_column_names) == 1) {
+                                               print "'$database_index_name' 
index will be dropped automatically when $table_name.$column_name column is 
dropped\n" if $DEBUG;
+                                       }
+                                       elsif 
($drop_indexes->{$database_index_name}) {
+                                               print "'$database_index_name' 
index will be replaced\n" if $DEBUG;
+                                       }
+                                       else {
+                                               print "'$database_index_name' 
index will be dropped\n" if $DEBUG;
+                                               push 
@{$changes->{ALTER_INDEX}}, "ALTER TABLE `$table_name` DROP INDEX 
`$database_index_name`;";
+                                       }
+                               }
+                       }
+                       
+               }
+       }
+       
+       REFERENCE_CONSTRAINT: for my $constraint_name (sort { lc($a) cmp lc($b) 
} keys %{$reference_info->{CONSTRAINTS}}) {
+               my $reference_constraint = 
$reference_info->{CONSTRAINTS}{$constraint_name};
+               my $reference_index_table          = 
$reference_constraint->{index_table};
+               my $reference_index_column         = 
$reference_constraint->{index_column};
+               my $reference_parent_table         = 
$reference_constraint->{parent_table};
+               my $reference_parent_column        = 
$reference_constraint->{parent_column};
+               my $reference_statement            = 
$reference_constraint->{STATEMENT};
+               my $reference_on_update            = 
$reference_constraint->{OPTIONS}{ON_UPDATE} || '';
+               my $reference_on_delete            = 
$reference_constraint->{OPTIONS}{ON_DELETE} || '';
+               
+               my $database_constraint = 
$database_info->{CONSTRAINTS}{$constraint_name};
+               if ($database_constraint) {
+                       my $database_index_table         = 
$database_constraint->{index_table};
+                       my $database_index_column        = 
$database_constraint->{index_column};
+                       my $database_parent_table        = 
$database_constraint->{parent_table};
+                       my $database_parent_column       = 
$database_constraint->{parent_column};
+                       my $database_statement           = 
$database_constraint->{STATEMENT};
+                       my $database_on_update           = 
$database_constraint->{OPTIONS}{ON_UPDATE} || '';
+                       my $database_on_delete           = 
$database_constraint->{OPTIONS}{ON_DELETE} || '';
+                       
+                       if ($reference_index_table   ne $database_index_table ||
+                                $reference_index_column  ne 
$database_index_column ||
+                                $reference_parent_table  ne 
$database_parent_table ||
+                                $reference_parent_column ne 
$database_parent_column ||
+                                $reference_on_update     ne 
$database_on_update ||
+                                $database_on_delete      ne 
$database_on_delete) {
+                         
+                               if ($DEBUG) {
+                                       print "constraints are different:\n";
+                                       print "reference : 
$reference_statement\n";
+                                       print "database  : 
$database_statement\n";
+                               }
+                               
+                               push @{$changes->{DROP_FOREIGN_KEY}}, "ALTER 
TABLE `$database_index_table` DROP FOREIGN KEY `$constraint_name`;";
+                               
#$changes->{DROP_FOREIGN_KEY}{$database_index_table}{$database_index_column} = 
$database_constraint;
+                       }
+                       else {
+                               next REFERENCE_CONSTRAINT;
+                       }
+               }
+               else {
+                       print "constraint does not exist in database: 
$reference_statement\n" if ($DEBUG);
+               }
+               
+               $reference_constraint->{STATEMENT} = "ALTER TABLE 
`$reference_index_table` ADD $reference_statement;";
+               push @{$changes->{ADD_FOREIGN_KEY}}, $reference_constraint;
+               #push @{$changes->{ADD_FOREIGN_KEY}}, "ALTER TABLE 
`$reference_index_table` ADD $reference_statement;";
+               
#$changes->{ADD_FOREIGN_KEY}{$reference_index_table}{$reference_index_column} = 
$reference_constraint;
+       }
+       
+       # Check for extra constraints in database not in reference
+       for my $constraint_name (sort { lc($a) cmp lc($b) } keys 
%{$database_info->{CONSTRAINTS}}) {
+               my $database_constraint = 
$database_info->{CONSTRAINTS}{$constraint_name};
+               my $database_index_table = $database_constraint->{index_table};
+                       my $database_index_column = 
$database_constraint->{index_column};
+               if (!defined($reference_info->{CONSTRAINTS}{$constraint_name})) 
{
+                       if ($DEBUG) {
+                               print "constraint exists in database but not 
reference:\n" . format_data($database_constraint) . "\n" if ($DEBUG);
+                       }
+                       #$changes->{DROP_FOREIGN_KEY}{$constraint_name} = 1;
+                       
#$changes->{DROP_FOREIGN_KEY}{$database_index_table}{$database_index_column} = 
$database_constraint;
+                       push @{$changes->{DROP_FOREIGN_KEY}}, "ALTER TABLE 
`$database_index_table` DROP FOREIGN KEY `$constraint_name`;";
+               }
+       }
+       
+       ## Check compare table for columns not defined in base
+       #for my $database_column_name (keys %$database_columns) {
+       #       if (!defined($reference_columns->{$database_column_name})) {
+       #               
+       #               my @referenced_constraints = 
get_referenced_constraints($reference_database_name, $table_name, 
$database_column_name);
+       #               for my $constraint (@referenced_constraints) {
+       #                       my $constraint_name = 
$constraint->{CONSTRAINT_NAME};
+       #                       #push @{$changes->{DROP_FOREIGN_KEY}}, "ALTER 
IGNORE TABLE `$table_name` DROP FOREIGN KEY `$constraint_name`;";
+       #                       
$changes->{DROP_FOREIGN_KEY}{$table_name}{$constraint_name} = 1;
+       #               }
+       #               
+       #               my $referencing_constraint = 
get_referencing_constraint($reference_database_name, $table_name, 
$database_column_name);
+       #               if ($referencing_constraint) {
+       #                       my $constraint_name = 
$referencing_constraint->{CONSTRAINT_NAME};
+       #                       #push @{$changes->{DROP_FOREIGN_KEY}}, "ALTER 
IGNORE TABLE `$table_name` DROP FOREIGN KEY `$constraint_name`;";
+       #                       
$changes->{DROP_FOREIGN_KEY}{$table_name}{$constraint_name} = 1;
+       #               }
+       #               
+       #               print "$table_name table '$database_column_name' column 
does not exist in reference\n";
+       #               # ALTER TABLE `table_name` DROP `column_name`
+       #               push @{$changes->{DROP_COLUMN}}, "ALTER TABLE 
`$table_name` DROP `$database_column_name`;";
+       #       }
+       #}
+       
+       return $changes;
+}
+
+#/////////////////////////////////////////////////////////////////////////////
+
+=head2 compare_array_elements
+
+ Parameters  : $array_ref_1, $array_ref_2
+ Returns     : boolean (0 if different, 1 if identical, undef if error)
+ Description : Compares the elements of 2 arrays. Arrays are considered
+               identical if the number of elements in each array is identical
+               and each array contains all of the elements of the other array.
+               This subroutine does not consider order. It only compares arrays
+               containing scalar elements. If an array element is a reference
+               undef will be returned.
+
+=cut
+
+sub compare_array_elements {
+       my ($array_ref_1, $array_ref_2) = @_;
+       if (!defined($array_ref_1) || !defined($array_ref_2)) {
+               notify($ERRORS{'WARNING'}, 0, "array reference arguments were 
not supplied");
+               return;
+       }
+       my $type_1 = ref($array_ref_1);
+       my $type_2 = ref($array_ref_2);
+       if (!$type_1) {
+               notify($ERRORS{'WARNING'}, 0, "both arguments must be array 
references, 1st argument is not a reference");
+       }
+       elsif (!$type_2) {
+               notify($ERRORS{'WARNING'}, 0, "both arguments must be array 
references, 2nd argument is not a reference");
+       }
+       elsif ($type_1 ne 'ARRAY') {
+               notify($ERRORS{'WARNING'}, 0, "both arguments must be array 
references, 1st argument reference type: $type_1");
+       }
+       elsif ($type_2 ne 'ARRAY') {
+               notify($ERRORS{'WARNING'}, 0, "both arguments must be array 
references, 2nd argument reference type: $type_2");
+       }
+       
+       my @array_1 = @$array_ref_1;
+       my @array_2 = @$array_ref_2;
+       
+       my $array_size_1 = scalar(@array_1);
+       my $array_size_2 = scalar(@array_2);
+       if ($array_size_1 != $array_size_2) {
+               notify($ERRORS{'DEBUG'}, 0, "arrays sizes are different, 1st 
array: $array_size_1, 2nd array: $array_size_2");
+               return 0;
+       }
+       
+       if (grep { ref($_) } @array_1) {
+               notify($ERRORS{'WARNING'}, 0, "unable to compare arrays, 1st 
array contains a reference value");
+               return;
+       }
+       elsif (grep { ref($_) } @array_2) {
+               notify($ERRORS{'WARNING'}, 0, "unable to compare arrays, 2nd 
array contains a reference value");
+               return;
+       }
+       
+       my %hash_1 = map { $_ => 1 } @array_1;
+       my %hash_2 = map { $_ => 1 } @array_2;
+       
+       for my $key (keys %hash_1) {
+               if (!defined($hash_2{$key})) {
+                       notify($ERRORS{'DEBUG'}, 0, "array elements are 
different, 1st array has element containing '$key', 2nd array does not");
+                       return 0;
+               }
+       }
+       
+       for my $key (keys %hash_2) {
+               if (!defined($hash_1{$key})) {
+                       notify($ERRORS{'DEBUG'}, 0, "array elements are 
different, 2nd array has element containing '$key', 1st array does not");
+                       return 0;
+               }
+       }
+       
+       notify($ERRORS{'DEBUG'}, 0, "arrays contain identical elements");
+       return 1;
+}
+
+#/////////////////////////////////////////////////////////////////////////////
+
+=head2 update_database
+
+ Parameters  : 
+ Returns     : 
+ Description :
+
+=cut
+
+sub update_database {
+       my ($database_name, $changes) = @_;
+       if (!$database_name || !$changes) {
+               notify($ERRORS{'WARNING'}, 0, "database name and schema change 
hash arguments were not supplied");
+               return;
+       }
+       
+       my $mn_os = get_mn_os();
+       if (!$mn_os) {
+               setup_print_error("unable to create database, failed to 
retrieve OS object to control this management node");
+               return;
+       }
+       
+       # Check for duplicate drop foreign key statements
+       my @drop_foreign_key_statements;
+       my $foreign_key_hash = {};
+       for my $foreign_key_statement (@{$changes->{DROP_FOREIGN_KEY}}) {
+               my ($foreign_key) = $foreign_key_statement =~ /FOREIGN KEY 
`?([\w_\$]+)/;
+               if (!$foreign_key) {
+                       setup_print_warning("failed to parse foreign key 
statement: $foreign_key_statement\n");
+               }
+               if (!defined($foreign_key_hash->{$foreign_key})) {
+                       $foreign_key_hash->{$foreign_key} = 1;
+                       push @drop_foreign_key_statements, 
$foreign_key_statement;
+               }
+               else {
+                       print "duplicate drop foreign key: $foreign_key\n";
+               }
+       }
+       $changes->{DROP_FOREIGN_KEY} = \@drop_foreign_key_statements;
+       
+       
+       my @operations = (
+               'DROP_FOREIGN_KEY',
+               'CREATE_TABLE',
+               'ADD_COLUMN',
+               'CHANGE_COLUMN',
+               'INSERT',
+               #'UPDATE',
+               'DROP_COLUMN',
+               'DROP_TABLE',
+               'ADD_INDEX',
+               'ALTER_INDEX',
+               #'MODIFY_COLUMN',
+       );
+       
+       for my $operation (@operations) {
+               my $temp_sql_file_path = 
"/tmp/$database_name\_$timestamp\_$operation.sql";
+               
+               my $statements = $changes->{$operation};
+               next unless $statements;
+               
+               my $statement_count = scalar(@$statements);
+               if (!$statement_count) {
+                       next;
+               }
+               
+               
+               for my $statement (@$statements) {
+                       if (database_execute($statement, $database_name)) {
+                               print "executed statement: " . 
substr($statement, 0, 97);
+                               if (length($statement) > 97) {
+                                       print "...";
+                               }
+                               print "\n";
+                       }
+                       else {
+                               setup_print_warning("failed to execute 
statement:");
+                               print "$statement\n\n";
+                               exit;
+                       }
+               }
+       }
+       
+       my $temp_sql_file_path = 
"/tmp/$database_name\_$timestamp\_ADD_FOREIGN_KEY.sql";
+       my $add_constraint_count = 0;
+       CONSTRAINT: for my $constraint (@{$changes->{ADD_FOREIGN_KEY}}) {
+               my $index_table = $constraint->{index_table};
+               my $index_column = $constraint->{index_column};
+               my $parent_table = $constraint->{parent_table};
+               my $parent_column = $constraint->{parent_column};
+               
+               my $select_statement = <<EOF;
+SELECT DISTINCT
+$index_table.$index_column
+FROM
+$index_table
+WHERE
+$index_table.$index_column IS NOT NULL
+AND NOT EXISTS (
+   SELECT
+   $parent_table.$parent_column
+   FROM
+   $parent_table
+   WHERE
+   $parent_table.$parent_column = $index_table.$index_column
+)
+EOF
+               
+               my @rows = database_select($select_statement, $database_name);
+               if (@rows) {
+                       setup_print_warning("\nunable to add constraint: 
$index_table.$index_column --> $parent_table.$parent_column");
+                       setup_print_wrap("$index_table.$index_column contains 
the following values which do not have a corresponding 
$parent_table.$parent_column value:");
+                       
+                       for my $row (@rows) {
+                               print "$index_table.$index_column=" . 
$row->{$index_column} . "\n";
+                       }
+                       print "\n";
+                       #print format_data($constraint) . "\n\n";
+                       next CONSTRAINT;
+               }
+               
+               my $statement = $constraint->{STATEMENT};
+               if (database_execute($statement, $database_name)) {
+                       print "added constraint: $index_table.$index_column --> 
$parent_table.$parent_column\n";
+               }
+               else {
+                       setup_print_warning("failed to add constraint:");
+                       print "$statement\n\n";
+                       exit;
+               }
+       }
+       
+       return 1;
+}
+
+#//////////////////////////////////////////////////////////////////////////////
+
+=head2 dump_database_to_file
+
+ Parameters  : $database_name, $sql_file_path, @mysqldump_options (optional)
+ Returns     : boolean
+ Description : Uses mysqldump to dump a database to a file.
+
+=cut
+
+sub dump_database_to_file {
+       my $database_name = shift;
+       my $sql_file_path = shift;
+       if (!$database_name || !$sql_file_path) {
+               notify($ERRORS{'WARNING'}, 0, "database name and SQL file path 
arguments were not specified");
+               return;
+       }
+       
+       my @mysqldump_options_argument = @_;
+       
+       my $mn_os = get_mn_os();
+       
+       my @options = (
+               "host=$DATABASE_SERVER",
+               "user=$DATABASE_USERNAME",
+               "password='$DATABASE_PASSWORD'",
+               #"result-file=$sql_file_path",
+               "databases $database_name",
+               "insert-ignore",
+               "order-by-primary",
+               "allow-keywords",                               # Allow 
creation of column names that are keywords
+               "flush-privileges",                     # Emit a FLUSH 
PRIVILEGES statement after dumping the mysql database
+               "skip-lock-tables",                     # Do not lock all 
tables to be dumped before dumping them
+               "skip-add-drop-table",          # Do not add a DROP TABLE 
statement before each CREATE TABLE statement
+               "skip-add-locks",                               # Do not 
surround each table dump with LOCK TABLES and UNLOCK TABLES statements
+               "skip-comments",                                # Do not write 
additional information in the dump file such as program version, server 
version, and host
+               "skip-disable-keys",                    # Do not surround the 
INSERT statements with /*!40000 ALTER TABLE tbl_name DISABLE KEYS */;
+               "skip-set-charset",                     # Do not add SET NAMES 
default_character_set to the output.
+               "skip-triggers",                                # Do not 
include triggers for each dumped table in the output
+               "skip-extended-insert",         # Use single-row INSERT 
statements
+               "complete-insert",                      # Use complete INSERT 
statements that include column names
+       );
+       
+       my $command = "mysqldump";
+       for my $option (@options, @mysqldump_options_argument) {
+               $command .= " ";
+               if ($option !~ /^-/) {
+                       $command .= "--";
+               }
+               $command .= $option;
+       }
+       $command .= " > $sql_file_path";
+       
+       print "\ndumping $database_name database to $sql_file_path...";
+       my ($exit_status, $output) = $mn_os->execute($command);
+       print "\n";
+       if (!defined($output)) {
+               setup_print_error("failed to execute command to dump 
$database_name database:\n$command");
+               return;
+       }
+       elsif ($exit_status ne '0') {
+               setup_print_error("failed to dump $database_name database to 
$sql_file_path, exit status: $exit_status\n\ncommand:\n$command\n\noutput:\n" . 
join("\n", @$output) . "\n");
+               
+               # Check for access denied error:
+               # ERROR 1044 (42000) at line 1: Access denied for user 
'<username>'@'<IP address>' to database '<database>'
+               # mysqldump: Got error: 1044: Access denied for user 
'<username>'@'<IP address>' to database '<database>' when selecting the database
+               if (my ($access_denied_line) = grep(/Access denied/i, 
@$output)) {
+                       my ($source_host) = $access_denied_line =~ 
/\@'([^']+)'/;
+                       $source_host = '*' if !defined($source_host);
+                       print "\nexecute the following command on database 
server $DATABASE_SERVER:\n";
+                       print "mysql -e \"GRANT 
SELECT,INSERT,UPDATE,DELETE,CREATE TEMPORARY TABLES ON $database_name.* TO 
'$DATABASE_USERNAME'\@'$source_host' IDENTIFIED BY '$DATABASE_PASSWORD';\"\n";
+               }
+               return;
+       }
+       else {
+               print "done.\n";
+       }
+       
+       # Add the command used to the output file
+       $mn_os->append_text_file($sql_file_path, "/*\n$command\n*/\n");
+       
+       print "\n";
+       return 1;
+}
+
+#//////////////////////////////////////////////////////////////////////////////
+
+=head2 setup
+
+ Parameters  : none
+ Returns     : 
+ Description : 
+
+=cut
+
+sub setup {
+       push @{$ENV{setup_path}}, 'Database Management';
+       
+       while (1) {
+               setup_print_break('=');
+               my $menu = setup_get_menu();
+               my $choice = setup_get_menu_choice($menu);
+               last unless defined $choice;
+               
+               my $choice_name = $choice->{name};
+               my $choice_sub_ref = $choice->{sub_ref};
+               my $choice_parent_menu_names = $choice->{parent_menu_names};
+               
+               push @{$ENV{setup_path}}, $choice_name;
+               
+               my $package_name = get_code_ref_package_name($choice_sub_ref);
+               my $subroutine_name = 
get_code_ref_subroutine_name($choice_sub_ref);
+               
+               setup_print_break('.');
+               &$choice_sub_ref();
+               
+               pop @{$ENV{setup_path}};
+       }
+}
+
+#//////////////////////////////////////////////////////////////////////////////
+
+=head2 setup_get_menu
+
+ Parameters  : none
+ Returns     : 
+ Description : 
+
+=cut
+
+sub setup_get_menu {
+       my $menu = {
+               'Database Management' => {
+                       'Upgrade Database' => \&setup_upgrade_database,
+                       'Backup Database' => \&setup_backup_database,
+               },
+       };
+       
+       return $menu;
+}
+
+#//////////////////////////////////////////////////////////////////////////////
+
+=head2 setup_select_database
+
+ Parameters  : $message (optional)
+ Returns     : string
+ Description : 
+
+=cut
+
+sub setup_select_database {
+       my ($message) = @_;
+       
+       my @database_names = get_database_names();
+       if (!@database_names) {
+               setup_print_error("failed to retrieve database names from 
database server");
+               return;
+       }
+       
+       # Remove special databases from array
+       @database_names = grep(!/^(mysql|information_schema)$/i, 
@database_names);
+       
+       if ($message) {
+               print "\n$message:\n";
+       }
+       else {
+               print "\nSelect database:\n";
+       }
+       
+       my $choice_index = setup_get_array_choice(@database_names);
+       return unless defined($choice_index);
+       
+       my $database_name = $database_names[$choice_index];
+       print "Selected database: $database_name\n";
+       
+       return $database_name;
+}
+
+#//////////////////////////////////////////////////////////////////////////////
+
+=head2 setup_backup_database
+
+ Parameters  : none
+ Returns     : 
+ Description : 
+
+=cut
+
+sub setup_backup_database {
+       my @database_names = get_database_names();
+       if (!@database_names) {
+               setup_print_error("failed to retrieve database names from 
database server");
+               return;
+       }
+       
+       my $database_name = setup_select_database('Select database to backup') 
|| return;
+       
+       my $default_backup_file_name = "$database_name\_$timestamp.sql";
+       my $default_backup_file_path = "/root/$default_backup_file_name";
+       
+       my $backup_file_path;
+       while (1) {
+               $backup_file_path = setup_get_input_file_path("\nEnter database 
backup file path\n", $default_backup_file_path);
+               return unless defined($backup_file_path);
+               
+               if (!$backup_file_path) {
+                       $backup_file_path = $default_backup_file_path;
+               }
+               elsif (-d $backup_file_path) {
+                       my $backup_directory_path = $backup_file_path;
+                       $backup_directory_path =~ s/[\/\\]*$//g;
+                       $backup_file_path = 
"$backup_directory_path/$default_backup_file_name";
+                       print "\nPath entered is a directory: 
$backup_directory_path\n";
+                       
+                       if (!setup_confirm("Use default file name? 
$backup_file_path", 'y')) {
+                               next;
+                       }
+               }
+               elsif (-e $backup_file_path) {
+                       print "File already exists: $backup_file_path\n";
+                       next;
+               }
+               elsif ($backup_file_path !~ /\.sql$/i) {
+                       if (setup_confirm("Database backup file path does not 
end with '.sql', append this extension to file path? ($backup_file_path.sql)")) 
{
+                               $backup_file_path .= '.sql';
+                       }
+               }
+               last;
+       }
+       
+       my @ignored_tables;
+       if (setup_confirm("\nDo you want any tables to be ignored?", 'n')) {
+               my @database_tables = get_database_table_names($database_name);
+               if (!@database_tables) {
+                       setup_print_error("failed to retrieve table names from 
$database_name database");
+                       return;
+               }
+               
+               my %database_table_hash = map { $_ => {'name' => $_, 'ignored' 
=> ''} } @database_tables;
+               #print format_data(\%database_table_hash) . "\n";
+               
+               IGNORE_TABLE: while (1) {
+                       print "\nSelect tables to ignore:\n";
+                       my $table_name_choice = 
setup_get_hash_choice(\%database_table_hash, 'name', 'ignored');
+                       last IGNORE_TABLE if !defined($table_name_choice);
+                       
+                       my $table_ignored = 
$database_table_hash{$table_name_choice}{ignored};
+                       if ($table_ignored) {
+                               
$database_table_hash{$table_name_choice}{ignored} = '';
+                       }
+                       else {
+                               
$database_table_hash{$table_name_choice}{ignored} = '*';
+                       }
+               }
+               
+               for my $table_name (sort { lc($a) cmp lc($b) } keys 
%database_table_hash) {
+                       if ($database_table_hash{$table_name}{ignored}) {
+                               push @ignored_tables, $table_name;
+                       }
+               }
+       }
+       
+       print "\n$database_name database will be backed up to 
$backup_file_path\n";
+       print "Tables ignored: " . (@ignored_tables ? "\n   " . join("\n   ", 
@ignored_tables) : '<none>') . "\n";
+       return unless setup_confirm("Confirm");
+       
+       my @mysqldump_options = map { "--ignore-table=$database_name.$_" } 
@ignored_tables;
+       return dump_database_to_file($database_name, $backup_file_path, 
@mysqldump_options);
+}
+
+#//////////////////////////////////////////////////////////////////////////////
+
+=head2 setup_upgrade_database
+
+ Parameters  :
+ Returns     : 
+ Description : 
+
+=cut
+
+sub setup_upgrade_database {
+       my ($database_name, $reference_sql_file_path) = @_;
+       
+       if (!$database_name) {
+               my @database_names = get_database_names();
+               if (!@database_names) {
+                       setup_print_error("failed to retrieve database names 
from database server");
+                       return;
+               }
+               $database_name = setup_select_database('Select database to 
upgrade');
+               if (!$database_name) {
+                       print "database not selected\n";
+                       return;
+               }
+       }
+       
+       my ($database_sql_file_handle, $database_sql_file_path) = 
tempfile(CLEANUP => 1, SUFFIX => '.sql');
+       if (!dump_database_to_file($database_name, $database_sql_file_path, 
'--no-data')) {
+               setup_print_error("failed to dump '$database_name' database to 
file: $database_sql_file_path");
+               return;
+       }
+       
+       my $database_info = parse_sql_file($database_sql_file_path);
+       if (!$database_info) {
+               setup_print_error("failed to parse SQL file: 
$database_sql_file_path");
+               return;
+       }
+       unlink $database_sql_file_path;
+       print "\n";
+       
+       # Check if it looks like this scripts resides in complete copy of 
extracted VCL source
+       if (!defined($reference_sql_file_path)) {
+               # Get the path to this script and its parent directory
+               my $current_file_path = abs_path(__FILE__);
+               my ($current_file_name, $current_directory_path) = 
fileparse($current_file_path);
+               $current_directory_path =~ s/\/$//g;
+               
+               if ($current_file_path =~ 
m|^(.+)/managementnode/bin/$current_file_name$|) {
+                       my $reference_directory_path = $1;
+                       $reference_sql_file_path = 
"$reference_directory_path/mysql/vcl.sql";
+                       
+                       if (!verify_vcl_sql_file($reference_sql_file_path)) {
+                               $reference_sql_file_path = undef;
+                       }
+               }
+               if (!$reference_sql_file_path) {
+                       my $sql_file_location_message;
+                       $sql_file_location_message .= "Please enter the path to 
the vcl.sql file which was included with the VCL $VERSION source code.";
+                       $sql_file_location_message .= " This should be located 
where you extracted the source code in a directory named 'sql'.";
+                       $sql_file_location_message .= " The path to this file 
most likely ends with 'apache-VCL-$VERSION/mysql/vcl.sql'\n";
+                       setup_print_wrap($sql_file_location_message);
+                       
+                       while (!$reference_sql_file_path) {
+                               $reference_sql_file_path = 
setup_get_input_file_path("Enter path to vcl.sql file");
+                               return unless defined($reference_sql_file_path);
+                               
+                               print "\n";
+                               if 
(!verify_vcl_sql_file($reference_sql_file_path)) {
+                                       $reference_sql_file_path = undef;
+                                       return;
+                               }
+                       }
+               }
+       }
+       
+       my $reference_info = parse_sql_file($reference_sql_file_path);
+       if (!$reference_info) {
+               return;
+       }
+       
+       my $changes = compare_database_to_reference($database_info, 
$reference_info);
+       if (!$changes) {
+               return;
+       }
+       
+       return update_database($database_name, $changes);
+}
+
+#//////////////////////////////////////////////////////////////////////////////
+
+1;
\ No newline at end of file


Reply via email to