bzdbcopy.pl 7.29 KB
#!/usr/bin/perl -w
#
# bzdbcopy.pl - Copies data from one Bugzilla database to another. 
#
# Author: Max Kanat-Alexander <mkanat@bugzilla.org>
#
# The intended use of this script is to copy data from an installation
# running on one DB platform to an installation running on another
# DB platform.
#
# It must be run from the directory containing your Bugzilla installation.
# That means if this script is in the contrib/ directory, you should
# be running it as: ./contrib/bzdbcopy.pl
#
# Note: Both schemas must already exist and be IDENTICAL. (That is, 
# they must have both been created/updated by the same version of 
# checksetup.pl.) This script will DESTROY ALL CURRENT DATA in the 
# target database.
#
# Both Schemas must be at least from Bugzilla 2.19.3, but if you're
# running a Bugzilla from before 2.20rc2, you'll need the patch at:
# https://bugzilla.mozilla.org/show_bug.cgi?id=300311 in order to
# be able to run this script.
#
# Before you using it, you have to correctly set all the variables
# in the "User-Configurable Settings" section, below. The "SOURCE"
# settings are for the database you're copying from, and the "TARGET"
# settings are for the database you're copying to. The DB_TYPE is
# the name of a DB driver from the Bugzilla/DB/ directory.
#

use strict;
use lib ".";
use Bugzilla::DB;
use Bugzilla::Util;

#####################################################################
# User-Configurable Settings
#####################################################################

# Settings for the 'Source' DB that you are copying from.
use constant SOURCE_DB_TYPE => 'Mysql';
use constant SOURCE_DB_NAME => 'bugs';
use constant SOURCE_DB_USER => 'bugs';
use constant SOURCE_DB_PASSWORD => '';

# Settings for the 'Target' DB that you are copying to.
use constant TARGET_DB_TYPE => 'Pg';
use constant TARGET_DB_NAME => 'bugs';
use constant TARGET_DB_USER => 'bugs';
use constant TARGET_DB_PASSWORD => '';

#####################################################################
# MAIN SCRIPT
#####################################################################

print "Connecting to the '" . SOURCE_DB_NAME . "' source database on " 
      . SOURCE_DB_TYPE . "...\n";
my $source_db = Bugzilla::DB::_connect(SOURCE_DB_TYPE, 'localhost', 
    SOURCE_DB_NAME, undef, undef, SOURCE_DB_USER, SOURCE_DB_PASSWORD);

print "Connecting to the '" . TARGET_DB_NAME . "' target database on "
      . TARGET_DB_TYPE . "...\n";
my $target_db = Bugzilla::DB::_connect(TARGET_DB_TYPE, 'localhost', 
    TARGET_DB_NAME, undef, undef, TARGET_DB_USER, TARGET_DB_PASSWORD);

# We use the table list from the target DB, because if somebody
# has customized their source DB, we still want the script to work,
# and it may otherwise fail in that situation (that is, the tables
# may not exist in the target DB).
my @table_list = $target_db->bz_table_list_real();

# We don't want to copy over the bz_schema table's contents.
my $bz_schema_location = lsearch(\@table_list, 'bz_schema');
splice(@table_list, $bz_schema_location, 1) if $bz_schema_location > 0;

# We turn off autocommit on the target DB, because we're doing so
# much copying.
$target_db->{AutoCommit} = 0;
$target_db->{AutoCommit} == 0 
    || warn "Failed to disable autocommit on " . TARGET_DB_TYPE;
foreach my $table (@table_list) {
    my @serial_cols;
    print "Reading data from the source '$table' table on " 
          . SOURCE_DB_TYPE . "...\n";
    my @table_columns = $target_db->bz_table_columns_real($table);
    my $select_query = "SELECT " . join(',', @table_columns) . " FROM $table";
    my $data_in = $source_db->selectall_arrayref($select_query);

    my $insert_query = "INSERT INTO $table ( " . join(',', @table_columns) 
                       . " ) VALUES (";
    $insert_query .= '?,' foreach (@table_columns);
    # Remove the last comma.
    chop($insert_query);
    $insert_query .= ")";
    my $insert_sth = $target_db->prepare($insert_query);

    print "Clearing out the target '$table' table on " 
          . TARGET_DB_TYPE . "...\n";
    $target_db->do("DELETE FROM $table");
    
    print "Writing data to the target '$table' table on " 
          . TARGET_DB_TYPE . "...";
    foreach my $row (@$data_in) {
        # Each column needs to be bound separately, because
        # many columns need to be dealt with specially.
        my $colnum = 0;
        foreach my $column (@table_columns) {
            # bind_param args start at 1, but arrays start at 0.
            my $param_num = $colnum + 1;
            my $already_bound;

            # Certain types of columns need special handling.
            my $col_info = $source_db->bz_column_info($table, $column);
            if ($col_info && $col_info->{TYPE} eq 'LONGBLOB') {
                $insert_sth->bind_param($param_num, 
                    $row->[$colnum], $target_db->BLOB_TYPE);
                $already_bound = 1;
            }
            elsif ($col_info && $col_info->{TYPE} =~ /decimal/) {
                # In MySQL, decimal cols can be too long.
                my $col_type = $col_info->{TYPE};
                $col_type =~ /decimal\((\d+),(\d+)\)/;
                my ($precision, $decimals) = ($1, $2);
                # If it's longer than precision + decimal point
                if ( length($row->[$colnum]) > ($precision + 1) ) {
                    # Truncate it to the highest allowed value.
                    my $orig_value = $row->[$colnum];
                    $row->[$colnum] = '';
                    my $non_decimal = $precision - $decimals;
                    $row->[$colnum] .= '9' while ($non_decimal--);
                    $row->[$colnum] .= '.';
                    $row->[$colnum] .= '9' while ($decimals--);
                    print "Truncated value $orig_value to " . $row->[$colnum] 
                         . " for $table.$column.\n";
                }
            }
            elsif ($col_info && $col_info->{TYPE} =~ /DATETIME/i) {
                my $date = $row->[$colnum];
                # MySQL can have strange invalid values for Datetimes.
                $row->[$colnum] = '1901-01-01 00:00:00'
                    if $date && $date eq '0000-00-00 00:00:00';
            }

            $insert_sth->bind_param($param_num, $row->[$colnum])
                unless $already_bound;
            $colnum++;
        }

        $insert_sth->execute();
    }

    # PostgreSQL doesn't like it when you insert values into
    # a serial field; it doesn't increment the counter 
    # automatically.
    if ($target_db->isa('Bugzilla::DB::Pg')) {
        foreach my $column (@table_columns) {
            my $col_info = $source_db->bz_column_info($table, $column);
            if ($col_info && $col_info->{TYPE} =~ /SERIAL/i) {
                # Set the sequence to the current max value + 1.
                my ($max_val) = $target_db->selectrow_array(
                    "SELECT MAX($column) FROM $table");
                $max_val = 0 if !defined $max_val;
                $max_val++;
                print "\nSetting the next value for $table.$column to $max_val.";
                $target_db->do("SELECT pg_catalog.setval 
                                ('${table}_${column}_seq', $max_val, false)");
            }
        }
    }

    print "\n\n";
}

print "Committing changes to the target database...\n";
$target_db->commit;

print "All done! Make sure to run checksetup on the new DB.\n";
$source_db->disconnect;
$target_db->disconnect;
1;