Raw content of SchemaConverter # Module to ease mysql schema conversion # Author: Arne Stabenau # Usage: # Make a schema_converter with new ( source_dbh, target_dbh ) # source database should be filled, target is empty schema # For each target table there will be a transfer # Either with a self specified transfer function # with a custom select # from a renamed source table # from the same name source table # configure the transfer (and the order) with # table_rename( "oldname", "newname" ) # table_skip( "tablename" ) # do_first( "newtable1", "newtable2", "newtable3" ) # Each standard table transfer # Either do a custom select statement # or transfer columns with same name or renamed into each other # specify columns to omit in target or get error # column_rename( "tablename", "oldcolname", "newcolname" ) # column_skip( $targetdb, "table", "column" ) # custom_select( $targetdb, "tablename", # Each row may be modified (custom select or standard select) # specify a row_modifier function for the target table # It takes a list ref and returns a list ref with the modified values # ( you have to know the order of columns which come in have to go out db ) # set_row_modifier( "tablename", function_reference ) # # potentially large (target) tables should be declared with # big_table( "newtablename" ); package SchemaConverter; use strict; use DBI; use Data::Dumper; sub new { my ( $class, @args ) = @_; my $self = {}; bless $self, $class; $self->source_dbh( $args[0] ); $self->target_dbh( $args[1] ); $self->read_dbs(); return $self; } sub tmp_dir { my ( $self, $arg ) = @_; ( defined $arg ) && ( $self->{'tmp_dir'} = $arg ); return $self->{'tmp_dir'}; } sub source_dbh { my ( $self, $arg ) = @_; ( defined $arg ) && ( $self->{'source_dbh'} = $arg ); return $self->{'source_dbh'}; } sub target_dbh { my ( $self, $arg ) = @_; ( defined $arg ) && ( $self->{'target_dbh'} = $arg ); return $self->{'target_dbh'}; } sub close_dbh { my $self = shift; $self->source_dbh()->disconnect(); $self->target_dbh()->disconnect(); } sub transfer { my $self = shift; local *FH; my $tmpdir; if( ! defined $self->tmp_dir() ) { $self->close_dbh(); die( "No tmp_dir specified" ); } else { $tmpdir = $self->tmp_dir(); } # first we should check if all standard trnasfers can go # otherwise testing this is a pain $self->check_possible_transfer(); my @ordered_tables = $self->{targetdb}{dofirst}; my %all_tables = %{$self->{targetdb}{tables}}; for my $tablename ( @ordered_tables ) { delete $all_tables{$tablename}; } for my $tablename ( keys %all_tables ) { push( @ordered_tables, $tablename ); } for my $tablename ( @ordered_tables ) { my $skip = 0; print STDERR "Transfer $tablename "; open( FH, ">$tmpdir/$tablename.txt" ) or die "cant open dumpfile"; if( exists $self->{targetdb}{tables}{$tablename}{transfer} ) { my $transfunc = $self->{targetdb}{tables}{$tablename}{transfer}; &$transfunc( $self->source_dbh(), $self->target_dbh(), $tablename, \*FH ); } else { my $sourcetable; if( exists $self->{targetdb}{tables}{$tablename}{link} ) { $sourcetable = $self->{targetdb}{tables}{$tablename}{link}; if( $sourcetable eq "" ) { # skip this table $skip = 1; } } else { # find the sourcetable if( exists $self->{targetdb}{tables}{$tablename}{select} ) { # if we have custom select, sourcetable doesnt make sense $sourcetable = undef; } elsif( ! exists $self->{sourcedb}{tables}{$tablename} ) { die "Couldnt find source for $tablename. Enter empty sourcetable."; } else { $sourcetable = $tablename; } } if( ! $skip ) { $self->standard_table_transfer( $sourcetable, $tablename, \*FH ); } } close FH; if( ! $skip ) { $self->target_dbh->do( "load data infile '$tmpdir/$tablename.txt' into table $tablename" ); } # upload ? unlink "$tmpdir/$tablename.txt"; print STDERR " finished\n"; } # close databases } # this function checks if a standard transfer is possible. # custom selects and custom transfer functions are not checked # it will only return if it can do the transfer otherwise die sub check_possible_transfer { my $self = shift; for my $tablename ( keys %{$self->{targetdb}{tables}} ) { my $sourcetable; if( exists $self->{targetdb}{tables}{$tablename}{transfer} ) { next; } if( exists $self->{targetdb}{tables}{$tablename}{link} ) { $sourcetable = $self->{targetdb}{tables}{$tablename}{link}; if( $sourcetable eq "" ) { # skip this table next; } } else { # find the sourcetable if( exists $self->{targetdb}{tables}{$tablename}{select} ) { # custom select, no check next; } elsif( ! exists $self->{sourcedb}{tables}{$tablename} ) { die "Couldnt find source for $tablename. Enter empty sourcetable."; } else { $sourcetable = $tablename; } } my @newcols = @{$self->{targetdb}{tables}{$tablename}{columns}}; my @oldcols = @{$self->{sourcedb}{tables}{$sourcetable}{columns}}; my %rename; if( exists $self->{targetdb}{tables}{$tablename}{columnrename} ) { %rename = %{$self->{targetdb}{tables}{$tablename}{columnrename}}; } else { %rename = (); } # find all source columns and build select statement for my $colname ( @newcols ) { my $selname; if( exists $rename{$colname} ) { $selname = $rename{$colname}; if( $selname eq "" ) { $selname = "NULL"; } } else { my $colExists = 0; for my $oldcol ( @oldcols ) { if( $oldcol eq $colname ) { $selname = $colname; $colExists = 1; last; } } if( ! $colExists ) { die "Couldnt fill $tablename.$colname\n"; } } } } } sub standard_table_transfer { my ( $self, $sourcetable, $targettable, $tmpfile ) = @_; my $sourcedb = $self->source_dbh(); my $targetdb = $self->target_dbh(); # look for custom select my $select = ""; if( exists $self->{targetdb}{tables}{$targettable}{select} ) { $select = $self->{targetdb}{tables}{$targettable}{select}; } else { # check if all columns have matching names my @newcols = @{$self->{targetdb}{tables}{$targettable}{columns}}; my @oldcols = @{$self->{sourcedb}{tables}{$sourcetable}{columns}}; my %rename; if( exists $self->{targetdb}{tables}{$targettable}{columnrename} ) { %rename = %{$self->{targetdb}{tables}{$targettable}{columnrename}}; } else { %rename = (); } # find all source columns and build select statement for my $colname ( @newcols ) { my $selname; if( exists $rename{$colname} ) { $selname = $rename{$colname}; if( $selname eq "" ) { $selname = "NULL"; } } else { my $colExists = 0; for my $oldcol ( @oldcols ) { if( $oldcol eq $colname ) { $selname = $colname; $colExists = 1; last; } } if( ! $colExists ) { die "Couldnt fill $targettable.$colname\n"; } } $select .= " $selname,"; } chop( $select ); $select = "SELECT $select from $sourcetable"; } # MySQL specific ... # DBD doesn't use cursors; loads whole table during execute() # problem for large tables - "mysql_use_result" gets around this my $sth; if (defined $self->{targetdb}{tables}{$targettable}{its_a_big_un}) { $sth = $self->source_dbh()->prepare( $select , { mysql_use_result => 1 } ); } else { $sth = $self->source_dbh()->prepare( $select ); } $sth->execute(); my $row; if( exists $self->{targetdb}{tables}{$targettable}{row_modify} ) { my $rowmod = $self->{targetdb}{tables}{$targettable}{row_modify}; while( my $arref = $sth->fetchrow_arrayref() ) { $row = &{$rowmod}($arref); print $tmpfile ( join( "\t",@{$row} ),"\n" ); } } else { while( my $arref = $sth->fetchrow_arrayref() ) { $row = join( "\t", @$arref ); print $tmpfile "$row\n"; } } } sub read_dbs { my $self = shift; my $dbh; for my $db_name ('targetdb', 'sourcedb' ) { if( $db_name eq 'targetdb' ) { $dbh = $self->target_dbh(); } else { $dbh = $self->source_dbh(); } my $sth = $dbh->prepare( "show tables" ); $sth->execute(); while( my $arref = $sth->fetchrow_arrayref() ) { $self->{$db_name}{tables}{$arref->[0]} = {}; } my @tables = keys %{$self->{$db_name}{tables}}; for my $table ( @tables ) { $sth = $dbh->prepare( "show columns from $table" ); $sth->execute(); while( my $arref = $sth->fetchrow_arrayref () ) { push( @{$self->{$db_name}{tables}{$table}{columns}}, $arref->[0] ); } } } } sub table_rename { my ( $self, $oldtable, $newtable ) = @_; $self->{targetdb}{tables}{$newtable}{link} = $oldtable; } sub table_skip { my ( $self, $newtable ) = @_; $self->{targetdb}{tables}{$newtable}{link} = ""; } sub column_rename { my ( $self, $newtable, $oldcol, $newcol ) = @_; $self->{targetdb}{tables}{$newtable}{columnrename}{$newcol} = $oldcol; } sub column_skip { my ( $self, $newtable, $newcol ) = @_; $self->{targetdb}{tables}{$newtable}{columnrename}{$newcol} = ""; } sub custom_select { my ( $self, $newtable, $select ) = @_; $self->{targetdb}{tables}{$newtable}{select} = $select; } sub set_row_modifier { my ( $self, $newtable, $row_modifier ) = @_; $self->{targetdb}{tables}{$newtable}{row_modify} = $row_modifier; } sub clear_target { my $self = shift; for my $tablename ( keys %{$self->{targetdb}{tables}} ) { $self->target_dbh()->do( "delete from $tablename" ); } } sub big_table { my ( $self, $table ) = @_; $self->{targetdb}{tables}{$table}{its_a_big_un} = 1; } sub do_first { my $self = shift; my @ordered_table_list= @_; $self->{targetdb}{dofirst} = \@ordered_table_list; } 1;