Undocument lies
[dbsrgits/DBIx-Class.git] / lib / DBIx / Class / Schema / Versioned.pm
1 package # Hide from PAUSE
2   DBIx::Class::Version::Table;
3 use base 'DBIx::Class::Core';
4 use strict;
5 use warnings;
6
7 __PACKAGE__->table('dbix_class_schema_versions');
8
9 __PACKAGE__->add_columns
10     ( 'version' => {
11         'data_type' => 'VARCHAR',
12         'is_auto_increment' => 0,
13         'default_value' => undef,
14         'is_foreign_key' => 0,
15         'name' => 'version',
16         'is_nullable' => 0,
17         'size' => '10'
18         },
19       'installed' => {
20           'data_type' => 'VARCHAR',
21           'is_auto_increment' => 0,
22           'default_value' => undef,
23           'is_foreign_key' => 0,
24           'name' => 'installed',
25           'is_nullable' => 0,
26           'size' => '20'
27           },
28       );
29 __PACKAGE__->set_primary_key('version');
30
31 package # Hide from PAUSE
32   DBIx::Class::Version::TableCompat;
33 use base 'DBIx::Class::Core';
34 __PACKAGE__->table('SchemaVersions');
35
36 __PACKAGE__->add_columns
37     ( 'Version' => {
38         'data_type' => 'VARCHAR',
39         },
40       'Installed' => {
41           'data_type' => 'VARCHAR',
42           },
43       );
44 __PACKAGE__->set_primary_key('Version');
45
46 package # Hide from PAUSE
47   DBIx::Class::Version;
48 use base 'DBIx::Class::Schema';
49 use strict;
50 use warnings;
51
52 __PACKAGE__->register_class('Table', 'DBIx::Class::Version::Table');
53
54 package # Hide from PAUSE
55   DBIx::Class::VersionCompat;
56 use base 'DBIx::Class::Schema';
57 use strict;
58 use warnings;
59
60 __PACKAGE__->register_class('TableCompat', 'DBIx::Class::Version::TableCompat');
61
62
63 # ---------------------------------------------------------------------------
64
65 =head1 NAME
66
67 DBIx::Class::Schema::Versioned - DBIx::Class::Schema plugin for Schema upgrades
68
69 =head1 SYNOPSIS
70
71   package MyApp::Schema;
72   use base qw/DBIx::Class::Schema/;
73
74   our $VERSION = 0.001;
75
76   # load MyApp::Schema::CD, MyApp::Schema::Book, MyApp::Schema::DVD
77   __PACKAGE__->load_classes(qw/CD Book DVD/);
78
79   __PACKAGE__->load_components(qw/Schema::Versioned/);
80   __PACKAGE__->upgrade_directory('/path/to/upgrades/');
81
82
83 =head1 DESCRIPTION
84
85 This module provides methods to apply DDL changes to your database using SQL
86 diff files. Normally these diff files would be created using
87 L<DBIx::Class::Schema/create_ddl_dir>.
88
89 A table called I<dbix_class_schema_versions> is created and maintained by the
90 module. This is used to determine which version your database is currently at.
91 Similarly the $VERSION in your DBIC schema class is used to determine the
92 current DBIC schema version.
93
94 The upgrade is initiated manually by calling C<upgrade> on your schema object,
95 this will attempt to upgrade the database from its current version to the current
96 schema version using a diff from your I<upgrade_directory>. If a suitable diff is
97 not found then no upgrade is possible.
98
99 =head1 GETTING STARTED
100
101 Firstly you need to setup your schema class as per the L</SYNOPSIS>, make sure
102 you have specified an upgrade_directory and an initial $VERSION.
103
104 Then you'll need two scripts, one to create DDL files and diffs and another to perform
105 upgrades. Your creation script might look like a bit like this:
106
107   use strict;
108   use Pod::Usage;
109   use Getopt::Long;
110   use MyApp::Schema;
111
112   my ( $preversion, $help );
113   GetOptions(
114     'p|preversion:s'  => \$preversion,
115   ) or die pod2usage;
116
117   my $schema = MyApp::Schema->connect(
118     $dsn,
119     $user,
120     $password,
121   );
122   my $sql_dir = './sql';
123   my $version = $schema->schema_version();
124   $schema->create_ddl_dir( 'MySQL', $version, $sql_dir, $preversion );
125
126 Then your upgrade script might look like so:
127
128   use strict;
129   use MyApp::Schema;
130
131   my $schema = MyApp::Schema->connect(
132     $dsn,
133     $user,
134     $password,
135   );
136
137   if (!$schema->get_db_version()) {
138     # schema is unversioned
139     $schema->deploy();
140   } else {
141     $schema->upgrade();
142   }
143
144 The script above assumes that if the database is unversioned then it is empty
145 and we can safely deploy the DDL to it. However things are not always so simple.
146
147 if you want to initialise a pre-existing database where the DDL is not the same
148 as the DDL for your current schema version then you will need a diff which
149 converts the database's DDL to the current DDL. The best way to do this is
150 to get a dump of the database schema (without data) and save that in your
151 SQL directory as version 0.000 (the filename must be as with
152 L<DBIx::Class::Schema/ddl_filename>) then create a diff using your create DDL
153 script given above from version 0.000 to the current version. Then hand check
154 and if necessary edit the resulting diff to ensure that it will apply. Once you have
155 done all that you can do this:
156
157   if (!$schema->get_db_version()) {
158     # schema is unversioned
159     $schema->install("0.000");
160   }
161
162   # this will now apply the 0.000 to current version diff
163   $schema->upgrade();
164
165 In the case of an unversioned database the above code will create the
166 dbix_class_schema_versions table and write version 0.000 to it, then
167 upgrade will then apply the diff we talked about creating in the previous paragraph
168 and then you're good to go.
169
170 =cut
171
172 package DBIx::Class::Schema::Versioned;
173
174 use strict;
175 use warnings;
176 use base 'DBIx::Class::Schema';
177
178 use Carp::Clan qw/^DBIx::Class/;
179 use Time::HiRes qw/gettimeofday/;
180 use Try::Tiny;
181 use namespace::clean;
182
183 __PACKAGE__->mk_classdata('_filedata');
184 __PACKAGE__->mk_classdata('upgrade_directory');
185 __PACKAGE__->mk_classdata('backup_directory');
186 __PACKAGE__->mk_classdata('do_backup');
187 __PACKAGE__->mk_classdata('do_diff_on_init');
188
189
190 =head1 METHODS
191
192 =head2 upgrade_directory
193
194 Use this to set the directory your upgrade files are stored in.
195
196 =head2 backup_directory
197
198 Use this to set the directory you want your backups stored in (note that backups
199 are disabled by default).
200
201 =cut
202
203 =head2 install
204
205 =over 4
206
207 =item Arguments: $db_version
208
209 =back
210
211 Call this to initialise a previously unversioned database. The table 'dbix_class_schema_versions' will be created which will be used to store the database version.
212
213 Takes one argument which should be the version that the database is currently at. Defaults to the return value of L</schema_version>.
214
215 See L</getting_started> for more details.
216
217 =cut
218
219 sub install
220 {
221   my ($self, $new_version) = @_;
222
223   # must be called on a fresh database
224   if ($self->get_db_version()) {
225       $self->throw_exception("A versioned schema has already been deployed, try upgrade instead.\n");
226   }
227
228   # default to current version if none passed
229   $new_version ||= $self->schema_version();
230
231   if ($new_version) {
232     # create versions table and version row
233     $self->{vschema}->deploy;
234     $self->_set_db_version({ version => $new_version });
235   }
236 }
237
238 =head2 deploy
239
240 Same as L<DBIx::Class::Schema/deploy> but also calls C<install>.
241
242 =cut
243
244 sub deploy {
245   my $self = shift;
246   $self->next::method(@_);
247   $self->install();
248 }
249
250 =head2 create_upgrade_path
251
252 =over 4
253
254 =item Arguments: { upgrade_file => $file }
255
256 =back
257
258 Virtual method that should be overridden to create an upgrade file.
259 This is useful in the case of upgrading across multiple versions
260 to concatenate several files to create one upgrade file.
261
262 You'll probably want the db_version retrieved via $self->get_db_version
263 and the schema_version which is retrieved via $self->schema_version
264
265 =cut
266
267 sub create_upgrade_path {
268   ## override this method
269 }
270
271 =head2 ordered_schema_versions
272
273 =over 4
274
275 =item Returns: a list of version numbers, ordered from lowest to highest
276
277 =back
278
279 Virtual method that should be overridden to return an ordered list
280 of schema versions. This is then used to produce a set of steps to
281 upgrade through to achieve the required schema version.
282
283 You may want the db_version retrieved via $self->get_db_version
284 and the schema_version which is retrieved via $self->schema_version
285
286 =cut
287
288 sub ordered_schema_versions {
289   ## override this method
290 }
291
292 =head2 upgrade
293
294 Call this to attempt to upgrade your database from the version it
295 is at to the version this DBIC schema is at. If they are the same
296 it does nothing.
297
298 It will call L</ordered_schema_versions> to retrieve an ordered
299 list of schema versions (if ordered_schema_versions returns nothing
300 then it is assumed you can do the upgrade as a single step). It
301 then iterates through the list of versions between the current db
302 version and the schema version applying one update at a time until
303 all relevant updates are applied.
304
305 The individual update steps are performed by using
306 L</upgrade_single_step>, which will apply the update and also
307 update the dbix_class_schema_versions table.
308
309 =cut
310
311 sub upgrade {
312     my ($self) = @_;
313     my $db_version = $self->get_db_version();
314
315     # db unversioned
316     unless ($db_version) {
317         carp 'Upgrade not possible as database is unversioned. Please call install first.';
318         return;
319     }
320
321     # db and schema at same version. do nothing
322     if ( $db_version eq $self->schema_version ) {
323         carp "Upgrade not necessary\n";
324         return;
325     }
326
327     my @version_list = $self->ordered_schema_versions;
328
329     # if nothing returned then we preload with min/max
330     @version_list = ( $db_version, $self->schema_version )
331       unless ( scalar(@version_list) );
332
333     # catch the case of someone returning an arrayref
334     @version_list = @{ $version_list[0] }
335       if ( ref( $version_list[0] ) eq 'ARRAY' );
336
337     # remove all versions in list above the required version
338     while ( scalar(@version_list)
339         && ( $version_list[-1] ne $self->schema_version ) )
340     {
341         pop @version_list;
342     }
343
344     # remove all versions in list below the current version
345     while ( scalar(@version_list) && ( $version_list[0] ne $db_version ) ) {
346         shift @version_list;
347     }
348
349     # check we have an appropriate list of versions
350     if ( scalar(@version_list) < 2 ) {
351         die;
352     }
353
354     # do sets of upgrade
355     while ( scalar(@version_list) >= 2 ) {
356         $self->upgrade_single_step( $version_list[0], $version_list[1] );
357         shift @version_list;
358     }
359 }
360
361 =head2 upgrade_single_step
362
363 =over 4
364
365 =item Arguments: db_version - the version currently within the db
366
367 =item Arguments: target_version - the version to upgrade to
368
369 =back
370
371 Call this to attempt to upgrade your database from the
372 I<db_version> to the I<target_version>. If they are the same it
373 does nothing.
374
375 It requires an SQL diff file to exist in your I<upgrade_directory>,
376 normally you will have created this using L<DBIx::Class::Schema/create_ddl_dir>.
377
378 If successful the dbix_class_schema_versions table is updated with
379 the I<target_version>.
380
381 This method may be called repeatedly by the upgrade method to
382 upgrade through a series of updates.
383
384 =cut
385
386 sub upgrade_single_step
387 {
388   my ($self,
389       $db_version,
390       $target_version) = @_;
391
392   # db and schema at same version. do nothing
393   if ($db_version eq $target_version) {
394     carp "Upgrade not necessary\n";
395     return;
396   }
397
398   # strangely the first time this is called can
399   # differ to subsequent times. so we call it
400   # here to be sure.
401   # XXX - just fix it
402   $self->storage->sqlt_type;
403
404   my $upgrade_file = $self->ddl_filename(
405                                          $self->storage->sqlt_type,
406                                          $target_version,
407                                          $self->upgrade_directory,
408                                          $db_version,
409                                         );
410
411   $self->create_upgrade_path({ upgrade_file => $upgrade_file });
412
413   unless (-f $upgrade_file) {
414     carp "Upgrade not possible, no upgrade file found ($upgrade_file), please create one\n";
415     return;
416   }
417
418   carp "DB version ($db_version) is lower than the schema version (".$self->schema_version."). Attempting upgrade.\n";
419
420   # backup if necessary then apply upgrade
421   $self->_filedata($self->_read_sql_file($upgrade_file));
422   $self->backup() if($self->do_backup);
423   $self->txn_do(sub { $self->do_upgrade() });
424
425   # set row in dbix_class_schema_versions table
426   $self->_set_db_version({version => $target_version});
427 }
428
429 =head2 do_upgrade
430
431 This is an overwritable method used to run your upgrade. The freeform method
432 allows you to run your upgrade any way you please, you can call C<run_upgrade>
433 any number of times to run the actual SQL commands, and in between you can
434 sandwich your data upgrading. For example, first run all the B<CREATE>
435 commands, then migrate your data from old to new tables/formats, then
436 issue the DROP commands when you are finished. Will run the whole file as it is by default.
437
438 =cut
439
440 sub do_upgrade
441 {
442   my ($self) = @_;
443
444   # just run all the commands (including inserts) in order
445   $self->run_upgrade(qr/.*?/);
446 }
447
448 =head2 run_upgrade
449
450  $self->run_upgrade(qr/create/i);
451
452 Runs a set of SQL statements matching a passed in regular expression. The
453 idea is that this method can be called any number of times from your
454 C<do_upgrade> method, running whichever commands you specify via the
455 regex in the parameter. Probably won't work unless called from the overridable
456 do_upgrade method.
457
458 =cut
459
460 sub run_upgrade
461 {
462     my ($self, $stm) = @_;
463
464     return unless ($self->_filedata);
465     my @statements = grep { $_ =~ $stm } @{$self->_filedata};
466     $self->_filedata([ grep { $_ !~ /$stm/i } @{$self->_filedata} ]);
467
468     for (@statements)
469     {
470         $self->storage->debugobj->query_start($_) if $self->storage->debug;
471         $self->apply_statement($_);
472         $self->storage->debugobj->query_end($_) if $self->storage->debug;
473     }
474
475     return 1;
476 }
477
478 =head2 apply_statement
479
480 Takes an SQL statement and runs it. Override this if you want to handle errors
481 differently.
482
483 =cut
484
485 sub apply_statement {
486     my ($self, $statement) = @_;
487
488     $self->storage->dbh->do($_) or carp "SQL was: $_";
489 }
490
491 =head2 get_db_version
492
493 Returns the version that your database is currently at. This is determined by the values in the
494 dbix_class_schema_versions table that C<upgrade> and C<install> write to.
495
496 =cut
497
498 sub get_db_version
499 {
500     my ($self, $rs) = @_;
501
502     my $vtable = $self->{vschema}->resultset('Table');
503     my $version = try {
504       $vtable->search({}, { order_by => { -desc => 'installed' }, rows => 1 } )
505               ->get_column ('version')
506                ->next;
507     };
508     return $version || 0;
509 }
510
511 =head2 schema_version
512
513 Returns the current schema class' $VERSION
514
515 =cut
516
517 =head2 backup
518
519 This is an overwritable method which is called just before the upgrade, to
520 allow you to make a backup of the database. Per default this method attempts
521 to call C<< $self->storage->backup >>, to run the standard backup on each
522 database type.
523
524 This method should return the name of the backup file, if appropriate..
525
526 This method is disabled by default. Set $schema->do_backup(1) to enable it.
527
528 =cut
529
530 sub backup
531 {
532     my ($self) = @_;
533     ## Make each ::DBI::Foo do this
534     $self->storage->backup($self->backup_directory());
535 }
536
537 =head2 connection
538
539 Overloaded method. This checks the DBIC schema version against the DB version and
540 warns if they are not the same or if the DB is unversioned. It also provides
541 compatibility between the old versions table (SchemaVersions) and the new one
542 (dbix_class_schema_versions).
543
544 To avoid the checks on connect, set the environment var DBIC_NO_VERSION_CHECK or alternatively you can set the ignore_version attr in the forth argument like so:
545
546   my $schema = MyApp::Schema->connect(
547     $dsn,
548     $user,
549     $password,
550     { ignore_version => 1 },
551   );
552
553 =cut
554
555 sub connection {
556   my $self = shift;
557   $self->next::method(@_);
558   $self->_on_connect();
559   return $self;
560 }
561
562 sub _on_connect
563 {
564   my ($self) = @_;
565
566   my $conn_info = $self->storage->connect_info;
567   $self->{vschema} = DBIx::Class::Version->connect(@$conn_info);
568   my $conn_attrs = $self->{vschema}->storage->_dbic_connect_attributes || {};
569
570   my $vtable = $self->{vschema}->resultset('Table');
571
572   # useful when connecting from scripts etc
573   return if ($conn_attrs->{ignore_version} || ($ENV{DBIC_NO_VERSION_CHECK} && !exists $conn_attrs->{ignore_version}));
574
575   # check for legacy versions table and move to new if exists
576   unless ($self->_source_exists($vtable)) {
577     my $vtable_compat = DBIx::Class::VersionCompat->connect(@$conn_info)->resultset('TableCompat');
578     if ($self->_source_exists($vtable_compat)) {
579       $self->{vschema}->deploy;
580       map { $vtable->create({ installed => $_->Installed, version => $_->Version }) } $vtable_compat->all;
581       $self->storage->_get_dbh->do("DROP TABLE " . $vtable_compat->result_source->from);
582     }
583   }
584
585   my $pversion = $self->get_db_version();
586
587   if($pversion eq $self->schema_version)
588     {
589 #         carp "This version is already installed\n";
590         return 1;
591     }
592
593   if(!$pversion)
594     {
595         carp "Your DB is currently unversioned. Please call upgrade on your schema to sync the DB.\n";
596         return 1;
597     }
598
599   carp "Versions out of sync. This is " . $self->schema_version .
600     ", your database contains version $pversion, please call upgrade on your Schema.\n";
601 }
602
603 # is this just a waste of time? if not then merge with DBI.pm
604 sub _create_db_to_schema_diff {
605   my $self = shift;
606
607   my %driver_to_db_map = (
608                           'mysql' => 'MySQL'
609                          );
610
611   my $db = $driver_to_db_map{$self->storage->dbh->{Driver}->{Name}};
612   unless ($db) {
613     print "Sorry, this is an unsupported DB\n";
614     return;
615   }
616
617   unless (DBIx::Class::Optional::Dependencies->req_ok_for ('deploy')) {
618     $self->throw_exception("Unable to proceed without " . DBIx::Class::Optional::Dependencies->req_missing_for ('deploy') );
619   }
620
621   my $db_tr = SQL::Translator->new({
622                                     add_drop_table => 1,
623                                     parser => 'DBI',
624                                     parser_args => { dbh => $self->storage->dbh }
625                                    });
626
627   $db_tr->producer($db);
628   my $dbic_tr = SQL::Translator->new;
629   $dbic_tr->parser('SQL::Translator::Parser::DBIx::Class');
630   $dbic_tr->data($self);
631   $dbic_tr->producer($db);
632
633   $db_tr->schema->name('db_schema');
634   $dbic_tr->schema->name('dbic_schema');
635
636   # is this really necessary?
637   foreach my $tr ($db_tr, $dbic_tr) {
638     my $data = $tr->data;
639     $tr->parser->($tr, $$data);
640   }
641
642   my $diff = SQL::Translator::Diff::schema_diff($db_tr->schema, $db,
643                                                 $dbic_tr->schema, $db,
644                                                 { ignore_constraint_names => 1, ignore_index_names => 1, caseopt => 1 });
645
646   my $filename = $self->ddl_filename(
647                                          $db,
648                                          $self->schema_version,
649                                          $self->upgrade_directory,
650                                          'PRE',
651                                     );
652   my $file;
653   if(!open($file, ">$filename"))
654     {
655       $self->throw_exception("Can't open $filename for writing ($!)");
656       next;
657     }
658   print $file $diff;
659   close($file);
660
661   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";
662 }
663
664
665 sub _set_db_version {
666   my $self = shift;
667   my ($params) = @_;
668   $params ||= {};
669
670   my $version = $params->{version} ? $params->{version} : $self->schema_version;
671   my $vtable = $self->{vschema}->resultset('Table');
672
673   ##############################################################################
674   #                             !!! NOTE !!!
675   ##############################################################################
676   #
677   # The travesty below replaces the old nice timestamp format of %Y-%m-%d %H:%M:%S
678   # This is necessary since there are legitimate cases when upgrades can happen
679   # back to back within the same second. This breaks things since we relay on the
680   # ability to sort by the 'installed' value. The logical choice of an autoinc
681   # is not possible, as it will break multiple legacy installations. Also it is
682   # not possible to format the string sanely, as the column is a varchar(20).
683   # The 'v' character is added to the front of the string, so that any version
684   # formatted by this new function will sort _after_ any existing 200... strings.
685   my @tm = gettimeofday();
686   my @dt = gmtime ($tm[0]);
687   my $o = $vtable->create({
688     version => $version,
689     installed => sprintf("v%04d%02d%02d_%02d%02d%02d.%03.0f",
690       $dt[5] + 1900,
691       $dt[4] + 1,
692       $dt[3],
693       $dt[2],
694       $dt[1],
695       $dt[0],
696       $tm[1] / 1000, # convert to millisecs, format as up/down rounded int above
697     ),
698   });
699 }
700
701 sub _read_sql_file {
702   my $self = shift;
703   my $file = shift || return;
704
705   open my $fh, '<', $file or carp("Can't open upgrade file, $file ($!)");
706   my @data = split /\n/, join '', <$fh>;
707   close $fh;
708
709   @data = split /;/,
710      join '',
711        grep { $_ &&
712               !/^--/  &&
713               !/^(BEGIN|BEGIN TRANSACTION|COMMIT)/mi }
714          @data;
715
716   return \@data;
717 }
718
719 sub _source_exists
720 {
721     my ($self, $rs) = @_;
722
723     return try {
724       $rs->search(\'1=0')->cursor->next;
725       1;
726     } catch {
727       0;
728     };
729 }
730
731 1;
732
733
734 =head1 AUTHORS
735
736 Jess Robinson <castaway@desert-island.me.uk>
737 Luke Saunders <luke@shadowcatsystems.co.uk>
738
739 =head1 LICENSE
740
741 You may distribute this code under the same terms as Perl itself.