From: Peter Rabbitson Date: Tue, 26 Jan 2010 13:10:45 +0000 (+0000) Subject: Merge 'trunk' into 'multiple_version_upgrade' X-Git-Tag: v0.08116~31^2~1 X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?p=dbsrgits%2FDBIx-Class.git;a=commitdiff_plain;h=78aef8c46882755aeac9d178f4dbec3942ec284b;hp=3eee36aa81bcc92e08856b64a08ea099316b55ff Merge 'trunk' into 'multiple_version_upgrade' r8394@Thesaurus (orig r8381): frew | 2010-01-19 17:34:10 +0100 add test to ensure no tabs in perl files r8397@Thesaurus (orig r8384): frew | 2010-01-19 18:00:12 +0100 fix test to be an author dep r8398@Thesaurus (orig r8385): ribasushi | 2010-01-19 18:19:40 +0100 First round of detabification r8399@Thesaurus (orig r8386): frew | 2010-01-19 23:42:50 +0100 Add EOL test r8401@Thesaurus (orig r8388): ribasushi | 2010-01-20 08:32:39 +0100 Fix minor RSC bug r8402@Thesaurus (orig r8389): roman | 2010-01-20 15:47:26 +0100 Added a FAQ entry titled: How do I override a run time method (e.g. a relationship accessor)? r8403@Thesaurus (orig r8390): roman | 2010-01-20 16:31:41 +0100 Added myself as a contributor. r8408@Thesaurus (orig r8395): jhannah | 2010-01-21 06:48:14 +0100 Added FAQ: Custom methods in Result classes r8413@Thesaurus (orig r8400): frew | 2010-01-22 04:17:20 +0100 add _is_numeric to ::Row r8418@Thesaurus (orig r8405): ribasushi | 2010-01-22 11:00:05 +0100 Generalize autoinc/count test r8420@Thesaurus (orig r8407): ribasushi | 2010-01-22 11:11:49 +0100 Final round of detabify r8421@Thesaurus (orig r8408): ribasushi | 2010-01-22 11:12:54 +0100 Temporarily disable whitespace checkers r8426@Thesaurus (orig r8413): ribasushi | 2010-01-22 11:35:15 +0100 Moev failing regression test away from trunk r8431@Thesaurus (orig r8418): frew | 2010-01-22 17:05:12 +0100 fix name of _is_numeric to _is_column_numeric r8437@Thesaurus (orig r8424): ribasushi | 2010-01-26 09:33:42 +0100 Switch to Test::Exception r8438@Thesaurus (orig r8425): ribasushi | 2010-01-26 09:48:30 +0100 Test txn_scope_guard regression r8439@Thesaurus (orig r8426): ribasushi | 2010-01-26 10:10:11 +0100 Fix txn_begin on external non-AC coderef regression --- diff --git a/Changes b/Changes index ef4b4a3..7f3c217 100644 --- a/Changes +++ b/Changes @@ -24,6 +24,8 @@ Revision history for DBIx::Class - New MSSQL specific resultset attribute to allow hacky ordered subquery support - Fix nasty schema/dbhandle leak due to SQL::Translator + - Initial implementation of a mechanism for Schema::Version to + apply multiple step upgrades - Fix regression on externally supplied $dbh with AutoCommit=0 - FAQ "Custom methods in Result classes" - Cookbook POD fix for add_drop_table instead of add_drop_tables diff --git a/lib/DBIx/Class/Schema/Versioned.pm b/lib/DBIx/Class/Schema/Versioned.pm index c4daa0d..929e79b 100644 --- a/lib/DBIx/Class/Schema/Versioned.pm +++ b/lib/DBIx/Class/Schema/Versioned.pm @@ -181,7 +181,7 @@ use warnings; use base 'DBIx::Class::Schema'; use Carp::Clan qw/^DBIx::Class/; -use POSIX 'strftime'; +use Time::HiRes qw/gettimeofday/; __PACKAGE__->mk_classdata('_filedata'); __PACKAGE__->mk_classdata('upgrade_directory'); @@ -271,32 +271,129 @@ sub create_upgrade_path { ## override this method } +=head2 ordered_schema_versions + +=over 4 + +=item Returns: a list of version numbers, ordered from lowest to highest + +=back + +Virtual method that should be overriden to return an ordered list +of schema versions. This is then used to produce a set of steps to +upgrade through to achieve the required schema version. + +You may want the db_version retrieved via $self->get_db_version +and the schema_version which is retrieved via $self->schema_version + +=cut + +sub ordered_schema_versions { + ## override this method +} + =head2 upgrade -Call this to attempt to upgrade your database from the version it is at to the version -this DBIC schema is at. If they are the same it does nothing. +Call this to attempt to upgrade your database from the version it +is at to the version this DBIC schema is at. If they are the same +it does nothing. -It requires an SQL diff file to exist in you I, normally you will -have created this using L. +It will call L to retrieve an ordered +list of schema versions (if ordered_schema_versions returns nothing +then it is assumed you can do the upgrade as a single step). It +then iterates through the list of versions between the current db +version and the schema version applying one update at a time until +all relvant updates are applied. -If successful the dbix_class_schema_versions table is updated with the current -DBIC schema version. +The individual update steps are performed by using +L, which will apply the update and also +update the dbix_class_schema_versions table. =cut -sub upgrade -{ - my ($self) = @_; - my $db_version = $self->get_db_version(); +sub upgrade { + my ($self) = @_; + my $db_version = $self->get_db_version(); - # db unversioned - unless ($db_version) { - carp 'Upgrade not possible as database is unversioned. Please call install first.'; - return; - } + # db unversioned + unless ($db_version) { + 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; + } + + 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' ); + + # remove all versions in list above the required version + while ( scalar(@version_list) + && ( $version_list[-1] ne $self->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; + } + + # check we have an appropriate list of versions + if ( scalar(@version_list) < 2 ) { + die; + } + + # do sets of upgrade + while ( scalar(@version_list) >= 2 ) { + $self->upgrade_single_step( $version_list[0], $version_list[1] ); + shift @version_list; + } +} + +=head2 upgrade_single_step + +=over 4 + +=item Arguments: db_version - the version currently within the db + +=item Arguments: target_version - the version to upgrade to + +=back + +Call this to attempt to upgrade your database from the +I to the I. If they are the same it +does nothing. + +It requires an SQL diff file to exist in your I, +normally you will have created this using L. + +If successful the dbix_class_schema_versions table is updated with +the I. + +This method may be called repeatedly by the upgrade method to +upgrade through a series of updates. + +=cut + +sub upgrade_single_step +{ + my ($self, + $db_version, + $target_version) = @_; # db and schema at same version. do nothing - if ($db_version eq $self->schema_version) { + if ($db_version eq $target_version) { carp "Upgrade not necessary\n"; return; } @@ -309,7 +406,7 @@ sub upgrade my $upgrade_file = $self->ddl_filename( $self->storage->sqlt_type, - $self->schema_version, + $target_version, $self->upgrade_directory, $db_version, ); @@ -329,7 +426,7 @@ sub upgrade $self->txn_do(sub { $self->do_upgrade() }); # set row in dbix_class_schema_versions table - $self->_set_db_version; + $self->_set_db_version({version => $target_version}); } =head2 do_upgrade @@ -574,10 +671,33 @@ sub _set_db_version { my $version = $params->{version} ? $params->{version} : $self->schema_version; my $vtable = $self->{vschema}->resultset('Table'); - $vtable->create({ version => $version, - installed => strftime("%Y-%m-%d %H:%M:%S", gmtime()) - }); + ############################################################################## + # !!! 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 + ), + }); } sub _read_sql_file { diff --git a/t/94versioning.t b/t/94versioning.t index 2d286ef..58c25d3 100644 --- a/t/94versioning.t +++ b/t/94versioning.t @@ -28,67 +28,70 @@ BEGIN { if not DBIx::Class::Storage::DBI->_sqlt_version_ok; } +use lib qw(t/lib); +use DBICTest; # do not remove even though it is not used + +use_ok('DBICVersion_v1'); + my $version_table_name = 'dbix_class_schema_versions'; my $old_table_name = 'SchemaVersions'; my $ddl_dir = dir ('t', 'var'); +mkdir ($ddl_dir) unless -d $ddl_dir; + my $fn = { v1 => $ddl_dir->file ('DBICVersion-Schema-1.0-MySQL.sql'), v2 => $ddl_dir->file ('DBICVersion-Schema-2.0-MySQL.sql'), - trans => $ddl_dir->file ('DBICVersion-Schema-1.0-2.0-MySQL.sql'), + v3 => $ddl_dir->file ('DBICVersion-Schema-3.0-MySQL.sql'), + trans_v12 => $ddl_dir->file ('DBICVersion-Schema-1.0-2.0-MySQL.sql'), + trans_v23 => $ddl_dir->file ('DBICVersion-Schema-2.0-3.0-MySQL.sql'), }; -use lib qw(t/lib); -use DBICTest; # do not remove even though it is not used - -use_ok('DBICVersionOrig'); +my $schema_v1 = DBICVersion::Schema->connect($dsn, $user, $pass, { ignore_version => 1 }); +eval { $schema_v1->storage->dbh->do('drop table ' . $version_table_name) }; +eval { $schema_v1->storage->dbh->do('drop table ' . $old_table_name) }; -my $schema_orig = DBICVersion::Schema->connect($dsn, $user, $pass, { ignore_version => 1 }); -eval { $schema_orig->storage->dbh->do('drop table ' . $version_table_name) }; -eval { $schema_orig->storage->dbh->do('drop table ' . $old_table_name) }; - -is($schema_orig->ddl_filename('MySQL', '1.0', $ddl_dir), $fn->{v1}, 'Filename creation working'); +is($schema_v1->ddl_filename('MySQL', '1.0', $ddl_dir), $fn->{v1}, 'Filename creation working'); unlink( $fn->{v1} ) if ( -e $fn->{v1} ); -$schema_orig->create_ddl_dir('MySQL', undef, $ddl_dir); +$schema_v1->create_ddl_dir('MySQL', undef, $ddl_dir); ok(-f $fn->{v1}, 'Created DDL file'); -$schema_orig->deploy({ add_drop_table => 1 }); +$schema_v1->deploy({ add_drop_table => 1 }); -my $tvrs = $schema_orig->{vschema}->resultset('Table'); -is($schema_orig->_source_exists($tvrs), 1, 'Created schema from DDL file'); +my $tvrs = $schema_v1->{vschema}->resultset('Table'); +is($schema_v1->_source_exists($tvrs), 1, 'Created schema from DDL file'); # loading a new module defining a new version of the same table DBICVersion::Schema->_unregister_source ('Table'); -eval "use DBICVersionNew"; +use_ok('DBICVersion_v2'); -my $schema_upgrade = DBICVersion::Schema->connect($dsn, $user, $pass, { ignore_version => 1 }); +my $schema_v2 = DBICVersion::Schema->connect($dsn, $user, $pass, { ignore_version => 1 }); { unlink($fn->{v2}); - unlink($fn->{trans}); + unlink($fn->{trans_v12}); - is($schema_upgrade->get_db_version(), '1.0', 'get_db_version ok'); - is($schema_upgrade->schema_version, '2.0', 'schema version ok'); - $schema_upgrade->create_ddl_dir('MySQL', '2.0', $ddl_dir, '1.0'); - ok(-f $fn->{trans}, 'Created DDL file'); + is($schema_v2->get_db_version(), '1.0', 'get_db_version ok'); + is($schema_v2->schema_version, '2.0', 'schema version ok'); + $schema_v2->create_ddl_dir('MySQL', '2.0', $ddl_dir, '1.0'); + ok(-f $fn->{trans_v12}, 'Created DDL file'); - sleep 1; # remove this when TODO below is completed warnings_like ( - sub { $schema_upgrade->upgrade() }, + sub { $schema_v2->upgrade() }, qr/DB version .+? is lower than the schema version/, 'Warn before upgrade', ); - is($schema_upgrade->get_db_version(), '2.0', 'db version number upgraded'); + is($schema_v2->get_db_version(), '2.0', 'db version number upgraded'); lives_ok ( sub { - $schema_upgrade->storage->dbh->do('select NewVersionName from TestVersion'); + $schema_v2->storage->dbh->do('select NewVersionName from TestVersion'); }, 'new column created' ); warnings_exist ( - sub { $schema_upgrade->create_ddl_dir('MySQL', '2.0', $ddl_dir, '1.0') }, + sub { $schema_v2->create_ddl_dir('MySQL', '2.0', $ddl_dir, '1.0') }, [ qr/Overwriting existing DDL file - $fn->{v2}/, - qr/Overwriting existing diff file - $fn->{trans}/, + qr/Overwriting existing diff file - $fn->{trans_v12}/, ], 'An overwrite warning generated for both the DDL and the diff', ); @@ -114,6 +117,54 @@ my $schema_upgrade = DBICVersion::Schema->connect($dsn, $user, $pass, { ignore_v } +# repeat the v1->v2 process for v2->v3 before testing v1->v3 +DBICVersion::Schema->_unregister_source ('Table'); +use_ok('DBICVersion_v3'); + +my $schema_v3 = DBICVersion::Schema->connect($dsn, $user, $pass, { ignore_version => 1 }); +{ + unlink($fn->{v3}); + unlink($fn->{trans_v23}); + + is($schema_v3->get_db_version(), '2.0', 'get_db_version 2.0 ok'); + is($schema_v3->schema_version, '3.0', 'schema version 3.0 ok'); + $schema_v3->create_ddl_dir('MySQL', '3.0', $ddl_dir, '2.0'); + ok(-f $fn->{trans_v23}, 'Created DDL 2.0 -> 3.0 file'); + + warnings_exist ( + sub { $schema_v3->upgrade() }, + qr/DB version .+? is lower than the schema version/, + 'Warn before upgrade', + ); + + is($schema_v3->get_db_version(), '3.0', 'db version number upgraded'); + + lives_ok ( sub { + $schema_v3->storage->dbh->do('select ExtraColumn from TestVersion'); + }, 'new column created'); +} + +# now put the v1 schema back again +{ + # drop all the tables... + eval { $schema_v1->storage->dbh->do('drop table ' . $version_table_name) }; + eval { $schema_v1->storage->dbh->do('drop table ' . $old_table_name) }; + eval { $schema_v1->storage->dbh->do('drop table TestVersion') }; + + { + local $DBICVersion::Schema::VERSION = '1.0'; + $schema_v1->deploy; + } + is($schema_v1->get_db_version(), '1.0', 'get_db_version 1.0 ok'); +} + +# attempt v1 -> v3 upgrade +{ + local $SIG{__WARN__} = sub { warn if $_[0] !~ /Attempting upgrade\.$/ }; + $schema_v3->upgrade(); + is($schema_v3->get_db_version(), '3.0', 'db version number upgraded'); +} + # check behaviour of DBIC_NO_VERSION_CHECK env var and ignore_version connect attr { my $schema_version = DBICVersion::Schema->connect($dsn, $user, $pass); @@ -142,28 +193,25 @@ my $schema_upgrade = DBICVersion::Schema->connect($dsn, $user, $pass, { ignore_v } # attempt a deploy/upgrade cycle within one second -TODO: { - - local $TODO = 'To fix this properly the table must be extended with an autoinc column, mst will not accept anything less'; - - eval { $schema_orig->storage->dbh->do('drop table ' . $version_table_name) }; - eval { $schema_orig->storage->dbh->do('drop table ' . $old_table_name) }; - eval { $schema_orig->storage->dbh->do('drop table TestVersion') }; +{ + eval { $schema_v2->storage->dbh->do('drop table ' . $version_table_name) }; + eval { $schema_v2->storage->dbh->do('drop table ' . $old_table_name) }; + eval { $schema_v2->storage->dbh->do('drop table TestVersion') }; # this attempts to sleep until the turn of the second my $t = time(); sleep (int ($t) + 1 - $t); - diag ('Fast deploy/upgrade start: ', time() ); + note ('Fast deploy/upgrade start: ', time() ); { - local $DBICVersion::Schema::VERSION = '1.0'; - $schema_orig->deploy; + local $DBICVersion::Schema::VERSION = '2.0'; + $schema_v2->deploy; } local $SIG{__WARN__} = sub { warn if $_[0] !~ /Attempting upgrade\.$/ }; - $schema_upgrade->upgrade(); + $schema_v2->upgrade(); - is($schema_upgrade->get_db_version(), '2.0', 'Fast deploy/upgrade'); + is($schema_v2->get_db_version(), '3.0', 'Fast deploy/upgrade'); }; unless ($ENV{DBICTEST_KEEP_VERSIONING_DDL}) { diff --git a/t/lib/DBICVersionOrig.pm b/t/lib/DBICVersion_v1.pm similarity index 93% rename from t/lib/DBICVersionOrig.pm rename to t/lib/DBICVersion_v1.pm index 3bdc0e2..56c01e2 100644 --- a/t/lib/DBICVersionOrig.pm +++ b/t/lib/DBICVersion_v1.pm @@ -42,4 +42,8 @@ sub upgrade_directory return 't/var/'; } +sub ordered_schema_versions { + return('1.0','2.0','3.0'); +} + 1; diff --git a/t/lib/DBICVersionNew.pm b/t/lib/DBICVersion_v2.pm similarity index 100% copy from t/lib/DBICVersionNew.pm copy to t/lib/DBICVersion_v2.pm diff --git a/t/lib/DBICVersionNew.pm b/t/lib/DBICVersion_v3.pm similarity index 82% rename from t/lib/DBICVersionNew.pm rename to t/lib/DBICVersion_v3.pm index b6508ca..29caaae 100644 --- a/t/lib/DBICVersionNew.pm +++ b/t/lib/DBICVersion_v3.pm @@ -30,6 +30,14 @@ __PACKAGE__->add_columns 'is_foreign_key' => 0, 'is_nullable' => 1, 'size' => '20' + }, + 'ExtraColumn' => { + 'data_type' => 'VARCHAR', + 'is_auto_increment' => 0, + 'default_value' => undef, + 'is_foreign_key' => 0, + 'is_nullable' => 1, + 'size' => '20' } ); @@ -40,16 +48,11 @@ use base 'DBIx::Class::Schema'; use strict; use warnings; -our $VERSION = '2.0'; +our $VERSION = '3.0'; __PACKAGE__->register_class('Table', 'DBICVersion::Table'); __PACKAGE__->load_components('+DBIx::Class::Schema::Versioned'); __PACKAGE__->upgrade_directory('t/var/'); __PACKAGE__->backup_directory('t/var/backup/'); -#sub upgrade_directory -#{ -# return 't/var/'; -#} - 1;