#!/usr/bin/perl # Alternative Bugzilla database copy script # Correctly handles custom fields and does no foreign key removal # See help below $USAGE = ... or run this with --help # Lines marked with "#!" comment in the end modify the DB use strict; my ($from, $from_user, $from_password); my @ignore_tables; # Runs before any 'uses' BEGIN { my $USAGE = <load_all(); my %seen_tables = ( # Copied manually: 'fielddefs' => 1, # Ignored: 'series_data' => 1, 'bz_schema' => 1, 'attach_data' => 1, 'bugs_fulltext' => 1, 'logincookies' => 1, 'tokens' => 1, 'globalauth' => 1, 'series1' => 1, 'bug_user_map' => 1, 'old_profiles' => 1, 'qwe' => 1, ); # Ignore more tables $seen_tables{$_} = 1 for @ignore_tables; my %field_exclude = ( 'fieldvaluecontrol' => 'field_id', 'bugs_activity' => 'fieldid', 'profiles_activity' => 'fieldid', ); my $from = DBI->connect('DBI:mysql:database=bugs3new', 'bugzilla3', 'bugzilla3') || die "Can't connect to source DB"; my $to = Bugzilla->dbh || die "Can't connect to destination DB"; # Get table dependencies and autoincrement columns my $table_depends = {}; my $autoincrement = {}; my ($bz_schema) = $from->selectrow_array('SELECT schema_data FROM bz_schema'); $bz_schema =~ s/^\$VAR1 = //; $bz_schema = eval $bz_schema; die $@ if $@; for my $t (keys %$bz_schema) { for (my $i = 0; $i < @{$bz_schema->{$t}->{FIELDS}}; $i += 2) { my $f = $bz_schema->{$t}->{FIELDS}->[$i+1]; if ($f->{REFERENCES}) { $table_depends->{$t}->{$f->{REFERENCES}->{TABLE}} = 1; } elsif ($f->{TYPE} =~ /serial/i) { $autoincrement->{$t} = $bz_schema->{$t}->{FIELDS}->[$i]; } } } # All tables except ones for custom fields my @copy_tables = getseq([ sort keys %$bz_schema ], \%seen_tables, $table_depends); my $fielddefs = $from->selectall_hashref('SELECT * FROM fielddefs', 'name'); my $fieldsnew = $to->selectall_hashref('SELECT * FROM fielddefs WHERE name IN (\''.join('\',\'', keys %$fielddefs).'\')', 'name'); for (values %$fielddefs) { if ($_->{custom}) { if ($_->{type} == FIELD_TYPE_SINGLE_SELECT || $_->{type} == FIELD_TYPE_MULTI_SELECT) { push @copy_tables, $_->{name}; } if ($_->{type} == FIELD_TYPE_MULTI_SELECT) { push @copy_tables, 'bug_'.$_->{name}; } } } my (@skip_fields, $maxkey); print "Redefine existing fields\n"; $to->do('DELETE FROM profiles_activity'); #! $to->do('DELETE FROM fielddefs WHERE name IN (\''.join('\',\'', keys %$fieldsnew).'\')'); #! insertall_hashref($to, 'fielddefs', [ map { $fielddefs->{$_} } keys %$fieldsnew ]); #! delete $fielddefs->{$_} for keys %$fieldsnew; for (keys %$fielddefs) { if ($fielddefs->{$_}->{obsolete}) { print "Skip field $_ (y/n)? "; if ( =~ /^\s*y/is) { push @skip_fields, $_; next; } } if (/^cf_/) { print "Creating field $_\n"; my $field = Bugzilla::Field->create($fielddefs->{$_}); #! } else { insertall_hashref($to, 'fielddefs', [ $fielddefs->{$_} ]); #! } } # Alter fielddefs autoincrement value manually ($maxkey) = $to->selectrow_array('SELECT MAX(id) FROM fielddefs'); alter_sequence($to, 'fielddefs', 'id', $maxkey); $to->{RaiseError} = 0; $to->{PrintError} = 1; for my $table (@copy_tables) { print "Selecting $table\n"; my $data = $from->selectall_arrayref( 'SELECT * FROM '.$from->quote_identifier($table). ' WHERE '.($field_exclude{$table} ? $field_exclude{$table}.' NOT IN ('.join(',', map { $fielddefs->{$_}->{id} } @skip_fields).')' : '1'), {Slice=>{}} ); print "Erasing $table\n"; $to->do('DELETE FROM '.$to->quote_identifier($table)); #! @$data || next; my %from_keys = %{$data->[0]}; my @to_cols = @{ $to->selectcol_arrayref( "SELECT column_name FROM information_schema.columns". " WHERE table_catalog=current_database() and table_schema=current_schema() and table_name=?", undef, $table ) }; delete $from_keys{$_} for @to_cols; for my $bad_key (keys %from_keys) { print "Removing column $bad_key\n"; delete $_->{$bad_key} for @$data; } $maxkey = 0; if (my $ai = $autoincrement->{$table}) { for (@$data) { $maxkey = $_->{$ai} if $_->{$ai} > $maxkey; } } my @buf; my $n = 0; my $total = @$data; while (@$data) { @buf = splice @$data, 0, 1024; insertall_hashref($to, $table, \@buf); #! $n += @buf; print "\rInserting $table: $n/$total..."; } print "\n"; # Initialize auto-increment values if (my $ai = $autoincrement->{$table}) { alter_sequence($to, $table, $ai, $maxkey); #! } } sub alter_sequence { my ($dbh, $table, $field, $maxkey) = @_; $maxkey = int($maxkey)+1; if ($dbh->isa('Bugzilla::DB::Mysql')) { $dbh->do("ALTER TABLE `$table` AUTOINCREMENT=$maxkey"); } elsif ($dbh->isa('Bugzilla::DB::Pg')) { $dbh->do("ALTER SEQUENCE ${table}_${field}_seq RESTART WITH $maxkey"); } elsif ($dbh->isa('Bugzilla::DB::Oracle')) { my @sql = $dbh->_bz_real_schema->_get_create_seq_ddl($table, $field, $maxkey); $dbh->do($_) foreach @sql; } } # Insert an array of hashes into a database table sub insertall_hashref { my ($dbh, $table, $rows) = @_; return 0 unless $dbh && $table && $rows && ref($rows) eq 'ARRAY' && @$rows; my @f = keys %{$rows->[0]}; my $k = join "|", $table, @f, scalar @$rows; my $sth; if (!($sth = $dbh->{__insertall_hashref_cache}->{$k})) { my $q = substr($dbh->quote_identifier('a'), 0, 1); $sth = $dbh->prepare( "INSERT INTO $q$table$q ($q".join("$q,$q", @f)."$q) VALUES ". join(',', ('('.(join(',', ('?') x @f)).')') x scalar @$rows) ); if ($dbh->isa('Bugzilla::DB::Pg') || $dbh->isa('Bugzilla::DB::Oracle')) { for my $i (0 .. $#f) { if ($dbh->bz_column_info($table, $f[$i])->{TYPE} eq 'LONGBLOB') { for my $j (0 .. $#$rows) { $sth->bind_param(1 + $i + $j*@f, '', $dbh->BLOB_TYPE); } } } } $dbh->{__insertall_hashref_cache}->{$k} = $sth; } return $sth->execute(map { @$_{@f} } @$rows); } # Get node sequence from a dependency graph sub getseq { my ($seq, $seen, $dep) = @_; my @r; for (@$seq) { if (!$seen->{$_}) { $seen->{$_} = 1; push @r, getseq([ sort keys %{$dep->{$_} || {}} ], $seen, $dep); push @r, $_; } } return @r; }