reordered methods of Versioned.pm and factored the initialisation stuff from upgrade...
[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';
4 use strict;
5 use warnings;
6
7 __PACKAGE__->load_components(qw/ Core/);
8 __PACKAGE__->table('dbix_class_schema_versions');
9
10 __PACKAGE__->add_columns
11     ( 'version' => {
12         'data_type' => 'VARCHAR',
13         'is_auto_increment' => 0,
14         'default_value' => undef,
15         'is_foreign_key' => 0,
16         'name' => 'version',
17         'is_nullable' => 0,
18         'size' => '10'
19         },
20       'installed' => {
21           'data_type' => 'VARCHAR',
22           'is_auto_increment' => 0,
23           'default_value' => undef,
24           'is_foreign_key' => 0,
25           'name' => 'installed',
26           'is_nullable' => 0,
27           'size' => '20'
28           },
29       );
30 __PACKAGE__->set_primary_key('version');
31
32 package # Hide from PAUSE
33   DBIx::Class::Version::TableCompat;
34 use base 'DBIx::Class';
35 __PACKAGE__->load_components(qw/ Core/);
36 __PACKAGE__->table('SchemaVersions');
37
38 __PACKAGE__->add_columns
39     ( 'Version' => {
40         'data_type' => 'VARCHAR',
41         },
42       'Installed' => {
43           'data_type' => 'VARCHAR',
44           },
45       );
46 __PACKAGE__->set_primary_key('Version');
47
48 package # Hide from PAUSE
49   DBIx::Class::Version;
50 use base 'DBIx::Class::Schema';
51 use strict;
52 use warnings;
53
54 __PACKAGE__->register_class('Table', 'DBIx::Class::Version::Table');
55
56 package # Hide from PAUSE
57   DBIx::Class::VersionCompat;
58 use base 'DBIx::Class::Schema';
59 use strict;
60 use warnings;
61
62 __PACKAGE__->register_class('TableCompat', 'DBIx::Class::Version::TableCompat');
63
64
65 # ---------------------------------------------------------------------------
66
67 =head1 NAME
68
69 DBIx::Class::Schema::Versioned - DBIx::Class::Schema plugin for Schema upgrades
70
71 =head1 SYNOPSIS
72
73   package Library::Schema;
74   use base qw/DBIx::Class::Schema/;   
75   # load Library::Schema::CD, Library::Schema::Book, Library::Schema::DVD
76   __PACKAGE__->load_classes(qw/CD Book DVD/);
77
78   __PACKAGE__->load_components(qw/+DBIx::Class::Schema::Versioned/);
79   __PACKAGE__->upgrade_directory('/path/to/upgrades/');
80   __PACKAGE__->backup_directory('/path/to/backups/');
81
82
83 =head1 DESCRIPTION
84
85 This module is a component designed to extend L<DBIx::Class::Schema>
86 classes, to enable them to upgrade to newer schema layouts. To use this
87 module, you need to have called C<create_ddl_dir> on your Schema to
88 create your upgrade files to include with your delivery.
89
90 A table called I<dbix_class_schema_versions> is created and maintained by the
91 module. This contains two fields, 'Version' and 'Installed', which
92 contain each VERSION of your Schema, and the date+time it was installed.
93
94 The actual upgrade is called manually by calling C<upgrade> on your
95 schema object. Code is run at connect time to determine whether an
96 upgrade is needed, if so, a warning "Versions out of sync" is
97 produced.
98
99 So you'll probably want to write a script which generates your DDLs and diffs
100 and another which executes the upgrade.
101
102 NB: At the moment, only SQLite and MySQL are supported. This is due to
103 spotty behaviour in the SQL::Translator producers, please help us by
104 them.
105
106
107 =head1 GETTING STARTED
108
109
110 =cut
111
112 =head1 METHODS
113
114 =head2 upgrade_directory
115
116 Use this to set the directory your upgrade files are stored in.
117
118 =head2 backup_directory
119
120 Use this to set the directory you want your backups stored in.
121
122 =cut
123
124 package DBIx::Class::Schema::Versioned;
125
126 use strict;
127 use warnings;
128 use base 'DBIx::Class';
129 use POSIX 'strftime';
130 use Data::Dumper;
131
132 __PACKAGE__->mk_classdata('_filedata');
133 __PACKAGE__->mk_classdata('upgrade_directory');
134 __PACKAGE__->mk_classdata('backup_directory');
135 __PACKAGE__->mk_classdata('do_backup');
136 __PACKAGE__->mk_classdata('do_diff_on_init');
137
138 =head2 install
139
140 =over 4
141
142 =item Arguments: $db_version
143
144 =back
145
146 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.
147
148 Takes one argument which should be the version that the database is currently at. Defaults to the return value of L</schema_version>.
149
150 See L</getting_started> for more details.
151
152 =cut
153
154 sub install
155 {
156   my ($self, $new_version) = @_;
157
158   # must be called on a fresh database
159   if ($self->get_db_version()) {
160     warn 'Install not possible as versions table already exists in database';
161   }
162
163   # default to current version if none passed
164   $new_version ||= $self->schema_version();
165
166   unless ($new_version) {
167     # create versions table and version row
168     $self->{vschema}->deploy;
169     $self->_set_db_version;
170   }
171 }
172
173 =head2 upgrade
174
175 Call this to attempt to upgrade your database from the version it is at to the version
176 this DBIC schema is at. 
177
178 It requires an SQL diff file to exist in $schema->upgrade_directory, normally you will
179 have created this using $schema->create_ddl_dir.
180
181 =cut
182
183 sub upgrade
184 {
185   my ($self) = @_;
186   my $db_version = $self->get_db_version();
187
188   # db unversioned
189   unless ($db_version) {
190     warn 'Upgrade not possible as database is unversioned. Please call install first.';
191     return;
192   }
193
194   # db and schema at same version. do nothing
195   if ($db_version eq $self->schema_version) {
196     print "Upgrade not necessary\n";
197     return;
198   }
199
200   # strangely the first time this is called can
201   # differ to subsequent times. so we call it 
202   # here to be sure.
203   # XXX - just fix it
204   $self->storage->sqlt_type;
205   
206   my $upgrade_file = $self->ddl_filename(
207                                          $self->storage->sqlt_type,
208                                          $self->schema_version,
209                                          $self->upgrade_directory,
210                                          $db_version,
211                                         );
212
213   unless (-f $upgrade_file) {
214     warn "Upgrade not possible, no upgrade file found ($upgrade_file), please create one\n";
215     return;
216   }
217
218   # backup if necessary then apply upgrade
219   $self->_filedata($self->_read_sql_file($upgrade_file));
220   $self->backup() if($self->do_backup);
221   $self->txn_do(sub { $self->do_upgrade() });
222
223   # set row in dbix_class_schema_versions table
224   $self->_set_db_version;
225 }
226
227 =head2 do_upgrade
228
229 This is an overwritable method used to run your upgrade. The freeform method
230 allows you to run your upgrade any way you please, you can call C<run_upgrade>
231 any number of times to run the actual SQL commands, and in between you can
232 sandwich your data upgrading. For example, first run all the B<CREATE>
233 commands, then migrate your data from old to new tables/formats, then 
234 issue the DROP commands when you are finished. Will run the whole file as it is by default.
235
236 =cut
237
238 sub do_upgrade
239 {
240   my ($self) = @_;
241
242   # just run all the commands (including inserts) in order                                                        
243   $self->run_upgrade(qr/.*?/);
244 }
245
246 =head2 run_upgrade
247
248  $self->run_upgrade(qr/create/i);
249
250 Runs a set of SQL statements matching a passed in regular expression. The
251 idea is that this method can be called any number of times from your
252 C<upgrade> method, running whichever commands you specify via the
253 regex in the parameter. Probably won't work unless called from the overridable
254 do_upgrade method.
255
256 =cut
257
258 sub run_upgrade
259 {
260     my ($self, $stm) = @_;
261
262     return unless ($self->_filedata);
263     my @statements = grep { $_ =~ $stm } @{$self->_filedata};
264     $self->_filedata([ grep { $_ !~ /$stm/i } @{$self->_filedata} ]);
265
266     for (@statements)
267     {      
268         $self->storage->debugobj->query_start($_) if $self->storage->debug;
269         $self->storage->dbh->do($_) or warn "SQL was:\n $_";
270         $self->storage->debugobj->query_end($_) if $self->storage->debug;
271     }
272
273     return 1;
274 }
275
276 =head2 get_db_version
277
278 Returns the version that your database is currently at. This is determined by the values in the
279 dbix_class_schema_versions table that $self->upgrade writes to.
280
281 =cut
282
283 sub get_db_version
284 {
285     my ($self, $rs) = @_;
286
287     my $vtable = $self->{vschema}->resultset('Table');
288     my $version = 0;
289     eval {
290       my $stamp = $vtable->get_column('installed')->max;
291       $version = $vtable->search({ installed => $stamp })->first->version;
292     };
293     return $version;
294 }
295
296 =head2 schema_version
297
298 Returns the current schema class' $VERSION
299
300 =cut
301
302 =head2 backup
303
304 This is an overwritable method which is called just before the upgrade, to
305 allow you to make a backup of the database. Per default this method attempts
306 to call C<< $self->storage->backup >>, to run the standard backup on each
307 database type. 
308
309 This method should return the name of the backup file, if appropriate..
310
311 This method is disabled by default. Set $schema->do_backup(1) to enable it.
312
313 =cut
314
315 sub backup
316 {
317     my ($self) = @_;
318     ## Make each ::DBI::Foo do this
319     $self->storage->backup($self->backup_directory());
320 }
321
322 =head2 connection
323
324 Overloaded method. This checks the DBIC schema version against the DB version and
325 warns if they are not the same or if the DB is unversioned. It also provides
326 compatibility between the old versions table (SchemaVersions) and the new one
327 (dbix_class_schema_versions).
328
329 To avoid the checks on connect, set the env var DBIC_NO_VERSION_CHECK or alternatively you can set the ignore_version attr in the forth arg like so:
330
331   my $schema = MyApp::Schema->connect(
332     $dsn,
333     $user,
334     $password,
335     { ignore_version => 1 },
336   );
337
338 =cut
339
340 sub connection {
341   my $self = shift;
342   $self->next::method(@_);
343   $self->_on_connect($_[3]);
344   return $self;
345 }
346
347 sub _on_connect
348 {
349   my ($self, $args) = @_;
350
351   $args = {} unless $args;
352   $self->{vschema} = DBIx::Class::Version->connect(@{$self->storage->connect_info()});
353   my $vtable = $self->{vschema}->resultset('Table');
354
355   # check for legacy versions table and move to new if exists
356   my $vschema_compat = DBIx::Class::VersionCompat->connect(@{$self->storage->connect_info()});
357   unless ($self->_source_exists($vtable)) {
358     my $vtable_compat = $vschema_compat->resultset('TableCompat');
359     if ($self->_source_exists($vtable_compat)) {
360       $self->{vschema}->deploy;
361       map { $vtable->create({ installed => $_->Installed, version => $_->Version }) } $vtable_compat->all;
362       $self->storage->dbh->do("DROP TABLE " . $vtable_compat->result_source->from);
363     }
364   }
365
366   # useful when connecting from scripts etc
367   return if ($args->{ignore_version} || ($ENV{DBIC_NO_VERSION_CHECK} && !exists $args->{ignore_version}));
368   my $pversion = $self->get_db_version();
369
370   if($pversion eq $self->schema_version)
371     {
372 #         warn "This version is already installed\n";
373         return 1;
374     }
375
376   if(!$pversion)
377     {
378         warn "Your DB is currently unversioned. Please call upgrade on your schema to sync the DB.\n";
379         return 1;
380     }
381
382   warn "Versions out of sync. This is " . $self->schema_version . 
383     ", your database contains version $pversion, please call upgrade on your Schema.\n";
384 }
385
386 # is this just a waste of time? if not then merge with DBI.pm
387 sub _create_db_to_schema_diff {
388   my $self = shift;
389
390   my %driver_to_db_map = (
391                           'mysql' => 'MySQL'
392                          );
393
394   my $db = $driver_to_db_map{$self->storage->dbh->{Driver}->{Name}};
395   unless ($db) {
396     print "Sorry, this is an unsupported DB\n";
397     return;
398   }
399
400   eval 'require SQL::Translator "0.09"';
401   if ($@) {
402     $self->throw_exception("SQL::Translator 0.09 required");
403   }
404
405   my $db_tr = SQL::Translator->new({ 
406                                     add_drop_table => 1, 
407                                     parser => 'DBI',
408                                     parser_args => { dbh => $self->storage->dbh }
409                                    });
410
411   $db_tr->producer($db);
412   my $dbic_tr = SQL::Translator->new;
413   $dbic_tr->parser('SQL::Translator::Parser::DBIx::Class');
414   $dbic_tr = $self->storage->configure_sqlt($dbic_tr, $db);
415   $dbic_tr->data($self);
416   $dbic_tr->producer($db);
417
418   $db_tr->schema->name('db_schema');
419   $dbic_tr->schema->name('dbic_schema');
420
421   # is this really necessary?
422   foreach my $tr ($db_tr, $dbic_tr) {
423     my $data = $tr->data;
424     $tr->parser->($tr, $$data);
425   }
426
427   my $diff = SQL::Translator::Diff::schema_diff($db_tr->schema, $db, 
428                                                 $dbic_tr->schema, $db,
429                                                 { ignore_constraint_names => 1, ignore_index_names => 1, caseopt => 1 });
430
431   my $filename = $self->ddl_filename(
432                                          $db,
433                                          $self->schema_version,
434                                          $self->upgrade_directory,
435                                          'PRE',
436                                     );
437   my $file;
438   if(!open($file, ">$filename"))
439     {
440       $self->throw_exception("Can't open $filename for writing ($!)");
441       next;
442     }
443   print $file $diff;
444   close($file);
445
446   print "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";
447 }
448
449
450 sub _set_db_version {
451   my $self = shift;
452
453   my $vtable = $self->{vschema}->resultset('Table');
454   $vtable->create({ version => $self->schema_version,
455                       installed => strftime("%Y-%m-%d %H:%M:%S", gmtime())
456                       });
457
458 }
459
460 sub _read_sql_file {
461   my $self = shift;
462   my $file = shift || return;
463
464   my $fh;
465   open $fh, "<$file" or warn("Can't open upgrade file, $file ($!)");
466   my @data = split(/\n/, join('', <$fh>));
467   @data = grep(!/^--/, @data);
468   @data = split(/;/, join('', @data));
469   close($fh);
470   @data = grep { $_ && $_ !~ /^-- / } @data;
471   @data = grep { $_ !~ /^(BEGIN|BEGIN TRANSACTION|COMMIT)/m } @data;
472   return \@data;
473 }
474
475 sub _source_exists
476 {
477     my ($self, $rs) = @_;
478
479     my $c = eval {
480         $rs->search({ 1, 0 })->count;
481     };
482     return 0 if $@ || !defined $c;
483
484     return 1;
485 }
486
487 1;
488
489
490 =head1 AUTHORS
491
492 Jess Robinson <castaway@desert-island.demon.co.uk>
493 Luke Saunders <luke@shadowcatsystems.co.uk>
494
495 =head1 LICENSE
496
497 You may distribute this code under the same terms as Perl itself.