X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FDBIx%2FClass%2FDeploymentHandler.pm;h=e78d07f87f000126308fa1004e16c1a30743788e;hb=cf400f483c5280a6d302f715b0d730ae829a4523;hp=cc15cdce330de6b6359f0edaa71a457690e7dace;hpb=b974984a4aa4db356fe2ebe90fb7e039da71197c;p=dbsrgits%2FDBIx-Class-DeploymentHandler.git diff --git a/lib/DBIx/Class/DeploymentHandler.pm b/lib/DBIx/Class/DeploymentHandler.pm index cc15cdc..e78d07f 100644 --- a/lib/DBIx/Class/DeploymentHandler.pm +++ b/lib/DBIx/Class/DeploymentHandler.pm @@ -2,381 +2,355 @@ package DBIx::Class::DeploymentHandler; use Moose; use Method::Signatures::Simple; -require DBIx::Class::Schema; # loaded for type constraint -require DBIx::Class::Storage; # loaded for type constraint -use Carp 'carp'; +require DBIx::Class::Schema; # loaded for type constraint +require DBIx::Class::Storage; # loaded for type constraint +require DBIx::Class::ResultSet; # loaded for type constraint +use Carp::Clan '^DBIx::Class::DeploymentHandler'; +use SQL::Translator; + +BEGIN { + use Moose::Util::TypeConstraints; + subtype 'DBIx::Class::DeploymentHandler::Databases' + => as 'ArrayRef[Str]'; + + coerce 'DBIx::Class::DeploymentHandler::Databases' + => from 'Str' + => via { [$_] }; + no Moose::Util::TypeConstraints; +} has schema => ( - isa => 'DBIx::Class::Schema', - is => 'ro', - required => 1, + isa => 'DBIx::Class::Schema', + is => 'ro', + required => 1, + handles => [qw( ddl_filename schema_version )], ); has upgrade_directory => ( - isa => 'Str', - is => 'ro', - required => 1, - default => 'sql', + isa => 'Str', + is => 'ro', + required => 1, + default => 'sql', ); has backup_directory => ( - isa => 'Str', - is => 'ro', + isa => 'Str', + is => 'ro', ); has storage => ( - isa => 'DBIx::Class::Storage', - is => 'ro', - lazy_build => 1, + isa => 'DBIx::Class::Storage', + is => 'ro', + lazy_build => 1, ); +method _build_storage { + my $s = $self->schema->storage; + $s->_determine_driver; + $s +} + has _filedata => ( - is => 'ro', + isa => 'ArrayRef[Str]', + is => 'rw', ); has do_backup => ( - isa => 'Bool', - is => 'ro', - default => undef, + isa => 'Bool', + is => 'ro', + default => undef, ); has do_diff_on_init => ( - isa => 'Bool', - is => 'ro', - default => undef, + isa => 'Bool', + is => 'ro', + default => undef, ); -method _build_storage { - return $self->schema->storage; +has version_rs => ( + isa => 'DBIx::Class::ResultSet', + is => 'ro', + lazy_build => 1, + handles => [qw( is_installed db_version )], +); + +has databases => ( + coerce => 1, + isa => 'DBIx::Class::DeploymentHandler::Databases', + is => 'ro', + default => sub { [qw( MySQL SQLite PostgreSQL )] }, +); + +has sqltargs => ( + isa => 'HashRef', + is => 'ro', + default => sub { {} }, +); + +method _build_version_rs { + $self->schema->set_us_up_the_bomb; + $self->schema->resultset('__VERSION') } +method backup { $self->storage->backup($self->backup_directory) } + method install($new_version) { - # must be called on a fresh database - if ($self->get_db_version) { - carp 'Install not possible as versions table already exists in database'; - } + carp 'Install not possible as versions table already exists in database' + if $self->is_installed; - # default to current version if none passed - $new_version ||= $self->schema_version(); + $new_version ||= $self->schema_version; if ($new_version) { - # create versions table and version row - $self->{vschema}->deploy; - $self->_set_db_version({ version => $new_version }); - } -} + $self->schema->deploy; -method deploy { - $self->next::method(@_); - $self->install(); + $self->version_rs->create({ + version => $new_version, + # ddl => $ddl, + # upgrade_sql => $upgrade_sql, + }); + } } -sub create_upgrade_path { - ## override this method -} +method create_upgrade_path { } -sub ordered_schema_versions { - ## override this method -} +method ordered_schema_versions { undef } method upgrade { - my $db_version = $self->get_db_version(); + my $db_version = $self->db_version; + my $schema_version = $self->schema_version; - # db unversioned unless ($db_version) { - carp 'Upgrade not possible as database is unversioned. Please call install first.'; - return; + # croak? + carp 'Upgrade not possible as database is unversioned. Please call install first.'; + return; } - # db and schema at same version. do nothing - if ( $db_version eq $self->schema_version ) { - carp "Upgrade not necessary\n"; - return; + if ( $db_version eq $schema_version ) { + # croak? + carp "Upgrade not necessary\n"; + return; } - my @version_list = $self->ordered_schema_versions; - - # if nothing returned then we preload with min/max - @version_list = ( $db_version, $self->schema_version ) - unless ( scalar(@version_list) ); - - # catch the case of someone returning an arrayref - @version_list = @{ $version_list[0] } - if ( ref( $version_list[0] ) eq 'ARRAY' ); + my @version_list = $self->ordered_schema_versions || + ( $db_version, $schema_version ); # remove all versions in list above the required version - while ( scalar(@version_list) - && ( $version_list[-1] ne $self->schema_version ) ) - { - pop @version_list; + while ( @version_list && ( $version_list[-1] ne $schema_version ) ) { + pop @version_list; } # remove all versions in list below the current version - while ( scalar(@version_list) && ( $version_list[0] ne $db_version ) ) { - shift @version_list; + while ( @version_list && ( $version_list[0] ne $db_version ) ) { + shift @version_list; } # check we have an appropriate list of versions - if ( scalar(@version_list) < 2 ) { - die; - } + die if @version_list < 2; # do sets of upgrade - while ( scalar(@version_list) >= 2 ) { - $self->upgrade_single_step( $version_list[0], $version_list[1] ); - shift @version_list; + while ( @version_list >= 2 ) { + $self->upgrade_single_step( $version_list[0], $version_list[1] ); + shift @version_list; } } method upgrade_single_step($db_version, $target_version) { - # db and schema at same version. do nothing if ($db_version eq $target_version) { + # croak? carp "Upgrade not necessary\n"; return; } - # strangely the first time this is called can - # differ to subsequent times. so we call it - # here to be sure. - # XXX - just fix it - $self->storage->sqlt_type; - my $upgrade_file = $self->ddl_filename( - $self->storage->sqlt_type, - $target_version, - $self->upgrade_directory, - $db_version, - ); + $self->storage->sqlt_type, + $target_version, + $self->upgrade_directory, + $db_version, + ); $self->create_upgrade_path({ upgrade_file => $upgrade_file }); unless (-f $upgrade_file) { + # croak? carp "Upgrade not possible, no upgrade file found ($upgrade_file), please create one\n"; return; } carp "DB version ($db_version) is lower than the schema version (".$self->schema_version."). Attempting upgrade.\n"; - # backup if necessary then apply upgrade - $self->_filedata($self->_read_sql_file($upgrade_file)); - $self->backup() if($self->do_backup); - $self->txn_do(sub { $self->do_upgrade() }); - - # set row in dbix_class_schema_versions table - $self->_set_db_version({version => $target_version}); -} + $self->_filedata($self->_read_sql_file($upgrade_file)); # I don't like this --fREW 2010-02-22 + $self->backup if $self->do_backup; + $self->schema->txn_do(sub { $self->do_upgrade }); -method do_upgrade { - # just run all the commands (including inserts) in order - $self->run_upgrade(qr/.*?/); + $self->version_rs->create({ + version => $target_version, + # ddl => $ddl, + # upgrade_sql => $upgrade_sql, + }); } -method run_upgrade($stm) { - return unless ($self->_filedata); - my @statements = grep { $_ =~ $stm } @{$self->_filedata}; - $self->_filedata([ grep { $_ !~ /$stm/i } @{$self->_filedata} ]); - - for (@statements) { - $self->storage->debugobj->query_start($_) if $self->storage->debug; - $self->apply_statement($_); - $self->storage->debugobj->query_end($_) if $self->storage->debug; - } +method create_ddl_dir($version, $preversion) { + my $schema = $self->schema; + my $databases = $self->databases; + my $dir = $self->upgrade_directory; + my $sqltargs = $self->sqltargs; + unless( -d $dir ) { + carp "Upgrade directory $dir does not exist, using ./\n"; + $dir = "./"; + } - return 1; -} + my $schema_version = $schema->schema_version || '1.x'; + $version ||= $schema_version; -method apply_statement($statement) { - $self->storage->dbh->do($_) or carp "SQL was: $_"; -} + $sqltargs = { + add_drop_table => 1, + ignore_constraint_names => 1, + ignore_index_names => 1, + %{$sqltargs || {}} + }; -method get_db_version($rs) { - my $vtable = $self->{vschema}->resultset('Table'); - my $version = eval { - $vtable->search({}, { order_by => { -desc => 'installed' }, rows => 1 } ) - ->get_column ('version') - ->next; - }; - return $version || 0; -} + my $sqlt = SQL::Translator->new( $sqltargs ); -method schema_version {} + $sqlt->parser('SQL::Translator::Parser::DBIx::Class'); + my $sqlt_schema = $sqlt->translate({ data => $schema }) + or $self->throw_exception ($sqlt->error); -method backup { - ## Make each ::DBI::Foo do this - $self->storage->backup($self->backup_directory()); -} + foreach my $db (@$databases) { + $sqlt->reset; + $sqlt->{schema} = $sqlt_schema; + $sqlt->producer($db); -method connection { - $self->next::method(@_); - $self->_on_connect($_[3]); - return $self; -} - -sub _on_connect -{ - my ($self, $args) = @_; + my $filename = $self->ddl_filename($db, $version, $dir); + if (-e $filename && ($version eq $schema_version )) { + # if we are dumping the current version, overwrite the DDL + carp "Overwriting existing DDL file - $filename"; + unlink $filename; + } - $args = {} unless $args; + my $output = $sqlt->translate; + if(!$output) { + carp("Failed to translate to $db, skipping. (" . $sqlt->error . ")"); + next; + } + my $file; + unless( open $file, q(>), $filename ) { + $self->throw_exception("Can't open $filename for writing ($!)"); + next; + } + print {$file} $output; + close $file; - $self->{vschema} = DBIx::Class::Version->connect(@{$self->storage->connect_info()}); - my $vtable = $self->{vschema}->resultset('Table'); + next unless $preversion; - # useful when connecting from scripts etc - return if ($args->{ignore_version} || ($ENV{DBIC_NO_VERSION_CHECK} && !exists $args->{ignore_version})); + require SQL::Translator::Diff; - # check for legacy versions table and move to new if exists - my $vschema_compat = DBIx::Class::VersionCompat->connect(@{$self->storage->connect_info()}); - unless ($self->_source_exists($vtable)) { - my $vtable_compat = $vschema_compat->resultset('TableCompat'); - if ($self->_source_exists($vtable_compat)) { - $self->{vschema}->deploy; - map { $vtable->create({ installed => $_->Installed, version => $_->Version }) } $vtable_compat->all; - $self->storage->dbh->do("DROP TABLE " . $vtable_compat->result_source->from); + my $prefilename = $self->ddl_filename($db, $preversion, $dir); + unless(-e $prefilename) { + carp("No previous schema file found ($prefilename)"); + next; } - } - - my $pversion = $self->get_db_version(); - if($pversion eq $self->schema_version) - { -# carp "This version is already installed\n"; - return 1; + my $diff_file = $self->ddl_filename($db, $version, $dir, $preversion); + if(-e $diff_file) { + carp("Overwriting existing diff file - $diff_file"); + unlink $diff_file; } - if(!$pversion) + my $source_schema; { - carp "Your DB is currently unversioned. Please call upgrade on your schema to sync the DB.\n"; - return 1; - } + my $t = SQL::Translator->new({ + %{$sqltargs}, + debug => 0, + trace => 0, + }); - carp "Versions out of sync. This is " . $self->schema_version . - ", your database contains version $pversion, please call upgrade on your Schema.\n"; -} + $t->parser( $db ) # could this really throw an exception? + or $self->throw_exception ($t->error); -sub _create_db_to_schema_diff { - my $self = shift; + my $out = $t->translate( $prefilename ) + or $self->throw_exception ($t->error); - my %driver_to_db_map = ( - 'mysql' => 'MySQL' - ); + $source_schema = $t->schema; - my $db = $driver_to_db_map{$self->storage->dbh->{Driver}->{Name}}; - unless ($db) { - print "Sorry, this is an unsupported DB\n"; - return; - } + $source_schema->name( $prefilename ) + unless $source_schema->name; + } - $self->throw_exception($self->storage->_sqlt_version_error) - if (not $self->storage->_sqlt_version_ok); + # The "new" style of producers have sane normalization and can support + # diffing a SQL file against a DBIC->SQLT schema. Old style ones don't + # And we have to diff parsed SQL against parsed SQL. + my $dest_schema = $sqlt_schema; - my $db_tr = SQL::Translator->new({ - add_drop_table => 1, - parser => 'DBI', - parser_args => { dbh => $self->storage->dbh } - }); + unless ( "SQL::Translator::Producer::$db"->can('preprocess_schema') ) { + my $t = SQL::Translator->new({ + %{$sqltargs}, + debug => 0, + trace => 0, + }); - $db_tr->producer($db); - my $dbic_tr = SQL::Translator->new; - $dbic_tr->parser('SQL::Translator::Parser::DBIx::Class'); - $dbic_tr->data($self); - $dbic_tr->producer($db); + $t->parser( $db ) # could this really throw an exception? + or $self->throw_exception ($t->error); - $db_tr->schema->name('db_schema'); - $dbic_tr->schema->name('dbic_schema'); + my $out = $t->translate( $filename ) + or $self->throw_exception ($t->error); - # is this really necessary? - foreach my $tr ($db_tr, $dbic_tr) { - my $data = $tr->data; - $tr->parser->($tr, $$data); - } + $dest_schema = $t->schema; - my $diff = SQL::Translator::Diff::schema_diff($db_tr->schema, $db, - $dbic_tr->schema, $db, - { ignore_constraint_names => 1, ignore_index_names => 1, caseopt => 1 }); - - my $filename = $self->ddl_filename( - $db, - $self->schema_version, - $self->upgrade_directory, - 'PRE', - ); - my $file; - if(!open($file, ">$filename")) - { - $self->throw_exception("Can't open $filename for writing ($!)"); - next; + $dest_schema->name( $filename ) + unless $dest_schema->name; } - print $file $diff; - close($file); - carp "WARNING: There may be differences between your DB and your DBIC schema. Please review and if necessary run the SQL in $filename to sync your DB.\n"; + my $diff = SQL::Translator::Diff::schema_diff( + $source_schema, $db, + $dest_schema, $db, + $sqltargs + ); + unless(open $file, q(>), $diff_file) { + $self->throw_exception("Can't write to $diff_file ($!)"); + next; + } + print {$file} $diff; + close $file; + } } +method do_upgrade { $self->run_upgrade(qr/.*?/) } -sub _set_db_version { - my $self = shift; - my ($params) = @_; - $params ||= {}; - - my $version = $params->{version} ? $params->{version} : $self->schema_version; - my $vtable = $self->{vschema}->resultset('Table'); - - ############################################################################## - # !!! NOTE !!! - ############################################################################## - # - # The travesty below replaces the old nice timestamp format of %Y-%m-%d %H:%M:%S - # This is necessary since there are legitimate cases when upgrades can happen - # back to back within the same second. This breaks things since we relay on the - # ability to sort by the 'installed' value. The logical choice of an autoinc - # is not possible, as it will break multiple legacy installations. Also it is - # not possible to format the string sanely, as the column is a varchar(20). - # The 'v' character is added to the front of the string, so that any version - # formatted by this new function will sort _after_ any existing 200... strings. - my @tm = gettimeofday(); - my @dt = gmtime ($tm[0]); - my $o = $vtable->create({ - version => $version, - installed => sprintf("v%04d%02d%02d_%02d%02d%02d.%03.0f", - $dt[5] + 1900, - $dt[4] + 1, - $dt[3], - $dt[2], - $dt[1], - $dt[0], - $tm[1] / 1000, # convert to millisecs, format as up/down rounded int above - ), - }); +method run_upgrade($stm) { + return unless $self->_filedata; + my @statements = grep { $_ =~ $stm } @{$self->_filedata}; + + for (@statements) { + $self->storage->debugobj->query_start($_) if $self->storage->debug; + $self->apply_statement($_); + $self->storage->debugobj->query_end($_) if $self->storage->debug; + } +} + +method apply_statement($statement) { + # croak? + $self->storage->dbh->do($_) or carp "SQL was: $_" } -sub _read_sql_file { - my $self = shift; - my $file = shift || return; +method _read_sql_file($file) { + return unless $file; open my $fh, '<', $file or carp("Can't open upgrade file, $file ($!)"); my @data = split /\n/, join '', <$fh>; close $fh; @data = grep { - $_ && - !/^--/ && - !/^(BEGIN|BEGIN TRANSACTION|COMMIT)/m + $_ && + !/^--/ && + !/^(BEGIN|BEGIN TRANSACTION|COMMIT)/m } split /;/, - join '', @data; + join '', @data; return \@data; } -sub _source_exists -{ - my ($self, $rs) = @_; - - my $c = eval { - $rs->search({ 1, 0 })->count; - }; - return 0 if $@ || !defined $c; +1; - return 1; -} +__END__ -1; +vim: ts=2,sw=2,expandtab