#!/usr/bin/perl

print STDERR "Depricated.  Use /app/tools/dbadmin/bin/diff_dbs.pl\n";
exit 1;

#
# This script compares two RPS databases, and produces a report describing how
# they differ.
#
# The report can be in two different formats:
#  -r : produces a 'regular', english report
#  -a : produces a set of SQL statements that, if executed, would transform the second
#       database's schema to match the first database.
#

use strict;
use Data::Dumper;
use Getopt::Std;



my @alters;
my $gReport = 0;
my $gAlter = 1;
my $gDebug = 0;
my $gDropTables = 0;

my %opt;
getopts('r:a:d', \%opt);

if (defined $opt{r})
{
    $gReport = $opt{r};
}

if (defined $opt{a})
{
    $gAlter = $opt{a};
}

if (defined $opt{d})
{
    $gDebug = 1;
}

if (defined $opt{t})
{
    $gDropTables = $opt{t};
}



my $db1Name = $ARGV[0];
my $db2Name = $ARGV[1];
if (! $db1Name || ! $db2Name)
{
    _usage();
}


my $schema1 = _getSchema($db1Name);
my $schema2 = _getSchema($db2Name);

_diffSchemas($schema1, $schema2);

if ($gAlter)
{
    print "--\n";
    print "-- ALTERS to bring $db2Name in sync with $db1Name\n";
    print "--\n\n";
    print "USE $db2Name ;\n\n";
    foreach my $alter (@alters)
    {
        print $alter . "\n";
    }
}


# ----------------------------------------



sub _usage
{
    print "This program compares the schemas of two databases.\n";
    print "USAGE:\n";
    print "$0 DATABASE1 DATABASE2 [-r] [-a] [-t] [-d]\n";
    print " (note - the '-' switches are boolean flags. Pass '1' to enable, '0' to disable)\n";
    print " -a : output SQL statements necessary to make DATABASE2 look like DATABASE1 (default)\n";
    print " -r : print a report detailing the differences\n";
    print " -t : If set to '1', output DROP TABLE statements in the Alter report\n";
    print " -d : produce debugging output: verbose and messy\n";


    exit(1);
}


sub _getSchema
{
    my ($name) = @_;

    # See if this is a filename (rather than a database name)
    #
    my $lines;
    if ($name =~ m/FILE:\/\/(.*)/)
    {
        $lines = _getLinesFromFile($1);
    }
    else
    {
        $lines = _getLinesFromDB($name);
    }


    my %schema;
    $schema{_dbName} = $name;

    
    # This parser uses a little 'state machine'.
    # We start out in the 'looking for table' state.
    # Once we find the start of a table definition, we transition into
    # the 'reading table' state.  Then back to 'looking for table' once
    # we hit the end of the table definition block.
    #
    my $kLookingForTable = 0;
    my $kReadingTable = 1;

    my $colIndex = 0;
    my $i = 0;
    my $state = $kLookingForTable;
    my $currentTable;
    for (my $i = 0; $i < scalar @$lines; $i++)
    {
        my $line = $lines->[$i];
        chomp $line;

        if (',' eq substr($line, -1))
        {
            substr($line, -1) = '';
        }

        if ($kLookingForTable == $state)
        {
            next unless $line =~ m/CREATE TABLE/;

            $line =~ m/\`(.*)\`/;
            $currentTable = $1;
            $state = $kReadingTable;
            $colIndex = 0;
            next;
        }

        if ($kReadingTable eq $state)
        {
            if ($line =~ m/\)(.*)\;/)
            {
                $schema{$currentTable}{_settings} = $1;
                $state = $kLookingForTable;
                next;
            }

            if ($line =~ m/PRIMARY KEY/)
            {
                $line =~ m/\((.*)\)/;
                $schema{$currentTable}{_keys}{_primaryKey} = $1;
                next;
            }

            if ($line =~ m/KEY/)
            {
                $line =~ m/\`(.*)\` \((.*)\)/;
                $schema{$currentTable}{_keys}{$1} = $2;
                next;
            }

            $line =~ m/\`(.*)\`(.*)/;
            $schema{$currentTable}{$1} = $2;
            $schema{$currentTable}{_order}{$colIndex} = $1;
            $colIndex++;

            next;
        }
    }

    return \%schema;
}

sub _getLinesFromFile
{
    my ($path) = @_;
    die "ERROR : $path does not exist\n" unless -e $path;
    die "ERROR : $path does not point to a regular file\n" unless -f $path;

    my @lines;
    open SCHEMA, $path or die "ERROR : could not open file $path for reading: $!\n";
    while (my $line = <SCHEMA>)
    {
        chomp $line;
        push @lines, _cleanLine($line);
    }


    return \@lines;
}

sub _getLinesFromDB
{
    my ($name) = @_;

    my $dump = `mysqldump -uroot -d $name`;

    if ($gDebug)
    {
        print "$dump\n";
    }

    my @nonCleanLines = split("\n", $dump);
    my @lines;
    foreach my $line (@nonCleanLines)
    {
        push @lines, _cleanLine($line);
    }

    return \@lines;
}


sub _cleanLine
{
    my ($inLine) = @_;

    my $kReadingChar = 0;
    my $kReadingQuotedPhrase = 1;
    my $kReadingWhiteSpace = 2;

    # I want to get rid of extraneous spaces, so that trivial differences in
    # white space won't make a difference.
    #
    my @chars = split(//, $inLine);
    my @newChars;
    my $state = $kReadingChar;
    for (my $i = 0; $i < scalar @chars; $i++)
    {
        if ($kReadingWhiteSpace == $state)
        {
            # skip over extra white space
            #
            if (_isWhiteSpace($chars[$i]))
            {
                next;
            }
            $state = $kReadingChar;
        }
        
        if ($kReadingChar == $state)
        {
            push @newChars, $chars[$i];
            if (_isQuote($chars[$i]))
            {
                $state = $kReadingQuotedPhrase;
            }
            elsif (_isWhiteSpace($chars[$i]))
            {
                $state = $kReadingWhiteSpace;
            }
            elsif (',' eq $chars[$i])
            {
                $state = $kReadingWhiteSpace;
            }
        }
        elsif ($kReadingQuotedPhrase == $state)
        {
            push @newChars, $chars[$i];

            # assuming no nested quotes...
            #
            if (_isQuote($chars[$i]))
            {
                $state = $kReadingChar;
            }
        }
    }

    my $outLine = join("", @newChars);
    return $outLine;
}

sub _isWhiteSpace
{
    my ($c) = @_;
    return 1 if ($c eq ' ');
    return 0;
}

sub _isQuote
{
    my ($c) = @_;
    return 1 if $c eq '\`';
    return 1 if $c eq '\'';
    return 1 if $c eq '\"';
    return 0;
}

sub _diffSchemas
{
    my ($schema1, $schema2) = @_;

    # First, see if either schema contains a table the other does not.
    #
    my @commonTables = _findCommonTables($schema1, $schema2);


    # Now compare the columns and keys in each common table.
    #
    foreach my $tableName (@commonTables)
    {
        _compareTable($tableName, $schema1, $schema2);
    }
}


sub _findCommonTables
{
    my ($schema1, $schema2) = @_;

    my %inBoth;
    foreach my $tableName (sort keys %$schema1)
    {
        next if ('_' eq substr($tableName, 0, 1));

        if (exists $schema2->{$tableName})
        {
            $inBoth{$tableName} = 1;
        }
        else
        {
            _report($schema2->{_dbName} . " missing table $tableName\n");
            _addTable($schema1, $tableName);
        }
    }
    foreach my $tableName (sort keys %$schema2)
    {
        next if ('_' eq substr($tableName, 0, 1));

        if (exists $schema1->{$tableName})
        {
            $inBoth{$tableName} = 1;
        }
        else
        {
            _report($schema1->{_dbName} . " missing table $tableName\n");

            if ($gDropTables)
            {
                _alter("DROP TABLE $tableName ;");
            }
        }
    }

    return sort keys %inBoth;
}


sub _compareTable
{
    my ($tableName, $schema1, $schema2) = @_;

    _report("\n-------- $tableName --------\n");

    my $padding = _max(length($schema1->{_dbName}), length($schema2->{_dbName})) + 1;

    my $table1 = $schema1->{$tableName};
    my $table2 = $schema2->{$tableName};


    # Iterate over the fields in the first table, and compare with the second.
    # We are looking for:
    # - fields that have been added
    # - fields that have been deleted
    # - fields that have been moved
    # - fields that have been renamed
    # So, the basic strategy is to first detect when there is a difference.
    # If there is a difference, is it simply a rename?
    #  If not, then is it a moved field?
    #    If not, then is this a new field?
    #      If not, then should we delete the old field?
    #
    # What if a field is renamed _and_ moved?  That seems rather hard to unambiguously detect.
    # - but, it may be possible.  If a field is missing, and there is a field with the same
    #   definition in the second table that does not exist in the first, we can perhaps assume this
    #   has been renamed and moved.
    #
    # If a field in a particular position has a diferrent name and a different definition, and none of
    # the other conditions are satisfied, then we can just alter 1 into the other, and let the database
    # do its best to convert the data.
    #
    
    # I am going to want to keep track of columns that I have already dealt with.
    # (by index).
    #
    my %touchedColumns;

    my $orderHash1 = $table1->{_order};
    my $orderHash2 = $table2->{_order};

    my $numColsTable1 = scalar keys %$orderHash1;

    my $i;
    for ($i = 0; $i < $numColsTable1; $i++)
    {
        _debugPrintHash("orderHash1: ", $orderHash1);
        _debugPrintHash("orderHash2: ", $orderHash2);

        my $col1 = $orderHash1->{$i};
        my $col2 = $orderHash2->{$i};

        _report("!!! i = $i  col1 = $col1  col2 = $col2\n");

        # If table2 does not even have a column in this slot (we've run off the end),
        # then just add it.
        #
        if (! defined $col2)
        {
            _report($schema2->{_dbName} . " missing column $col1\n");
            _alter("ALTER TABLE $tableName ADD COLUMN $col1 " . $table1->{$col1} . " ;");

            # !!! I don't really need to modify my schema structure anymore - from this point
            # !!! forward, any columns in table 1 will be brand new to table 2.
            #
            next;
        }


        # Do they have different names?
        #
        if ($col1 ne $col2)
        {
            #  Does this column exist elsewhere in table2?
            #
            if (exists $table2->{$col1})
            {
                _moveColumn($i, $tableName, $schema1, $schema2, $table1, $table2, $orderHash1, $orderHash2, $col1, $col2);
                next;
            }

            # Does the column in table2 exist elsewhere in table1?
            #
            if (exists $table1->{$col2})
            {
                _insertColumn($i, $tableName, $schema1, $schema2, $table1, $table2, $orderHash1, $orderHash2, $col1, $col2);
                next;
            }


            # This is a rename/redefine.
            #
            _redefineColumn($i, $tableName, $schema1, $schema2, $table1, $table2, $orderHash1, $orderHash2, $col1, $col2);
            next;
        }


        # Do they have different definitions?
        #
        if ($table1->{$col1} ne $table2->{$col1})
        {
            _redefineColumn($i, $tableName, $schema1, $schema2, $table1, $table2, $orderHash1, $orderHash2, $col1, $col2);
            next;
        }


        # They are identical. Nothing to do.
        #
    }


    # Any remaining columns in table2 should be deleted.
    #

    my $numColsTable2 = scalar keys %$orderHash2;

    for (my $j = $i; $j < $numColsTable2; $j++)
    {
        my $col2 = $orderHash2->{$j};
        _deleteColumn($i, $tableName, $schema2, $table2, $orderHash2, $col2);
    }


    # Now compare the keys.
    #
    my %sharedKeys;
    foreach my $key (sort keys %{$table1->{_keys}})
    {
        if (exists $table2->{_keys}{$key})
        {
            $sharedKeys{$key} = 1;
        }
        else
        {
            _report($schema2->{_dbName} . " missing key $key\n");

            if ('_primaryKey' eq $key)
            {
                _alter("ALTER TABLE $tableName ADD PRIMARY KEY (" . $table1->{_keys}{$key} . ") ;");
            }
            else
            {
                _alter("ALTER TABLE $tableName ADD KEY \`$key\` (" . $table1->{_keys}{$key} . ") ;");
            }
        }
    }

    foreach my $key (sort keys %{$table2->{_keys}})
    {
        if (exists $table1->{_keys}{$key})
        {
            $sharedKeys{$key} = 1;
        }
        else
        {
            _report($schema1->{_dbName} . " missing key $key\n");
            if ('_primaryKey' eq $key)
            {
                _alter("ALTER TABLE $tableName DROP PRIMARY KEY ;");
            }
            else
            {
                _alter("ALTER TABLE $tableName DROP KEY \`$key\` ;");
            }
        }
    }

    foreach my $key (sort keys %sharedKeys)
    {
        if ($table1->{_keys}{$key} ne $table2->{_keys}{$key})
        {
            if ('_primaryKey' eq $key)
            {
                _report("PRIMARY KEY:\n");
                _alter("ALTER TABLE $tableName DROP PRIMARY KEY ;");
                _alter("ALTER TABLE $tableName ADD PRIMARY KEY (" . $table1->{_keys}{$key} . ") ;");
            }
            else
            {
                _report("KEY $key\n");
                _alter("ALTER TABLE $tableName DROP KEY \`$key\` ;");
                _alter("ALTER TABLE $tableName ADD KEY \`$key\` (" . $table1->{_keys}{$key} . ") ;");
            }
            _report("\t" . _paddedString($schema1->{_dbName}, $padding) . ":" . $table1->{_keys}{$key}
            . "\n\t" . _paddedString($schema2->{_dbName}, $padding) . ":" . $table2->{_keys}{$key}
            . "\n");
        }
    }


    # Now see if the table settings (i.e. the last line in the table definition) match.
    # !!! Since we can't actually modify this with an alter in a consistant way, let's not bother.
    #
#    if ($table1->{_settings} ne $table2->{_settings})
#    {
#        _report("TABLE SETTINGS: " . $table1->{_settings} . " : " . $table2->{_settings});
#        _alter("ALTER TABLE $tableName " . $table1->{_settings} . " ;");
#    }
}

sub _paddedString
{
    my ($string, $size) = @_;

    my $outString = $string . (' ' x ($size - length($string)));
    return $outString;
}

sub _max
{
    my ($val1, $val2) = @_;

    return ($val1 > $val2 ? $val1 : $val2);
}

sub _report
{
    my ($string) = @_;

    if ($gReport)
    {
        print $string;
    }
} 

sub _alter
{
    my ($alter) = @_;

    if ($gAlter)
    {
        push @alters, $alter;
    }
}

sub _addTable
{
    my ($schema, $tableName) = @_;

    _alter("CREATE TABLE \`$tableName\` (");

    my $table = $schema->{$tableName};

    # columns first
    #
    foreach my $column (sort keys %$table)
    {
        next if ('_' eq substr($column, 0, 1));
        my $colDef = $table->{$column};
        my $alter = "\`$column\` $colDef,";
        _alter($alter);
    }


    # Then the keys.
    #
    my @keys;
    foreach my $key (keys %{$table->{_keys}})
    {
        my $def = $table->{_keys}{$key};
        if ('_primaryKey' eq $key)
        {
            push @keys, "PRIMARY KEY ($def)";
        }
        else
        {
            push @keys, "KEY \`$key\` ($def)";
        }
    }


    for (my $i = 0; $i < scalar @keys; $i++)
    {
        my $alter = $keys[$i];

        if ($i < ((scalar @keys) - 1))
        {
            $alter .= ',';
        }
        _alter($alter);
    }

    _alter(") ENGINE=MyISAM DEFAULT CHARSET=latin1 ;\n");
}


sub _moveColumn
{
    my ($i, $tableName, $schema1, $schema2, $table1, $table2, $orderHash1, $orderHash2, $col1, $col2) = @_;

    # We want to _move_ this column, possibly re-defining it as well.
    #
    my $moveTo;
    if (0 == $i)
    {
        $moveTo = 'FIRST';
    }
    else
    {
        my $lastIndex = $i - 1;
        $moveTo = 'AFTER ' . $orderHash1->{$lastIndex};
    }
    _report($schema2->{_dbName} . " moving column $col1 to $moveTo\n");
    _alter("ALTER TABLE $tableName MODIFY COLUMN $col1 " . $table1->{$col1} . " $moveTo ;");


    # Update our internal data structure to reflect this change.
    #
    $table2->{$col1} = $table1->{$col1};


    # Need to adjust table 2's order hash as well.
    # I need to shift everything above the column's old position down 1 slot.
    #
    # So, first find the old position.
    my $oldIndex;
    my $t2Size = scalar keys %$orderHash2;
    for (my $i2 = $i + 1; $i2 < $t2Size; $i2++)
    {
        if ($orderHash2->{$i2} eq $col1)
        {
            $oldIndex = $i2;
            last;
        }
    }

    # Now shift
    #
    for (my $j = $oldIndex; $j >= $i; $j--)
    {
        my $prev = $j - 1;
        $orderHash2->{$j} = $orderHash2->{$prev};
    }

    $orderHash2->{$i} = $col1;
}


sub _insertColumn
{
    my ($i, $tableName, $schema1, $schema2, $table1, $table2, $orderHash1, $orderHash2, $col1, $col2) = @_;


    # Figure out where this column is getting inserted.
    #
    my $insertAt;
    if (0 == $i)
    {
        $insertAt = 'FIRST';
    }
    else
    {
        my $lastIndex = $i - 1;
        $insertAt = 'AFTER ' . $orderHash1->{$lastIndex};
    }
    _report($schema2->{_dbName} . " inserting column $col1 at $insertAt\n");
    _alter("ALTER TABLE $tableName ADD COLUMN $col1 " . $table1->{$col1} . " $insertAt ;");


    # Now we need to adjust our internal representation.
    #
    # Shift everything from $i down 1
    #
    my $t2Size = scalar keys %$orderHash2;
    for (my $j = $t2Size; $j >= $i; $j--)
    {
        my $prev = $j - 1;
        $orderHash2->{$j} = $orderHash2->{$prev};
    }


    # Now insert in the order hash, and update the table2 model.
    #
    $orderHash2->{$i} = $col1;
    $table2->{$col1} = $table1->{$col1};
}


sub _redefineColumn
{
    my ($i, $tableName, $schema1, $schema2, $table1, $table2, $orderHash1, $orderHash2, $col1, $col2) = @_;

    my $newDef = "$col1 " . $table1->{$col1};
    _report($schema2->{_dbName} . " modifying column $col2 to $newDef\n");
    _alter("ALTER TABLE $tableName CHANGE COLUMN $col2 $newDef ;");


    # Update the internal representation.
    #
    delete $table2->{$col2};
    $table2->{$col1} = $table1->{$col1};
    $orderHash2->{$i} = $col1;
}

sub _deleteColumn
{
    my ($i, $tableName, $schema2, $table2, $orderHash2, $col2) = @_;

    _report($schema2->{_dbName} . " deleting column $col2\n");
    _alter("ALTER TABLE $tableName DROP COLUMN $col2 ;");
}

sub _debugPrintHash
{
    my ($string, $hash) = @_;
    _report($string . "\n");

    foreach my $key(sort {$a <=> $b} keys %$hash)
    {
        _report("$key => " . $hash->{$key} . "\n");
    }
}
