moved schema_version from Versioning to core
[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 =head1 METHODS
107
108 =head2 upgrade_directory
109
110 Use this to set the directory your upgrade files are stored in.
111
112 =head2 backup_directory
113
114 Use this to set the directory you want your backups stored in.
115
116 =cut
117
118 package DBIx::Class::Schema::Versioned;
119
120 use strict;
121 use warnings;
122 use base 'DBIx::Class';
123 use POSIX 'strftime';
124 use Data::Dumper;
125
126 __PACKAGE__->mk_classdata('_filedata');
127 __PACKAGE__->mk_classdata('upgrade_directory');
128 __PACKAGE__->mk_classdata('backup_directory');
129 __PACKAGE__->mk_classdata('do_backup');
130 __PACKAGE__->mk_classdata('do_diff_on_init');
131
132 =head2 get_db_version
133
134 Returns the version that your database is currently at. This is determined by the values in the
135 dbix_class_schema_versions table that $self->upgrade writes to.
136
137 =cut
138
139 sub get_db_version
140 {
141     my ($self, $rs) = @_;
142
143     my $vtable = $self->{vschema}->resultset('Table');
144     my $version = 0;
145     eval {
146       my $stamp = $vtable->get_column('installed')->max;
147       $version = $vtable->search({ installed => $stamp })->first->version;
148     };
149     return $version;
150 }
151
152 sub _source_exists
153 {
154     my ($self, $rs) = @_;
155
156     my $c = eval {
157         $rs->search({ 1, 0 })->count;
158     };
159     return 0 if $@ || !defined $c;
160
161     return 1;
162 }
163
164 =head2 backup
165
166 This is an overwritable method which is called just before the upgrade, to
167 allow you to make a backup of the database. Per default this method attempts
168 to call C<< $self->storage->backup >>, to run the standard backup on each
169 database type. 
170
171 This method should return the name of the backup file, if appropriate..
172
173 This method is disabled by default. Set $schema->do_backup(1) to enable it.
174
175 =cut
176
177 sub backup
178 {
179     my ($self) = @_;
180     ## Make each ::DBI::Foo do this
181     $self->storage->backup($self->backup_directory());
182 }
183
184 # is this just a waste of time? if not then merge with DBI.pm
185 sub _create_db_to_schema_diff {
186   my $self = shift;
187
188   my %driver_to_db_map = (
189                           'mysql' => 'MySQL'
190                          );
191
192   my $db = $driver_to_db_map{$self->storage->dbh->{Driver}->{Name}};
193   unless ($db) {
194     print "Sorry, this is an unsupported DB\n";
195     return;
196   }
197
198   eval 'require SQL::Translator "0.09"';
199   if ($@) {
200     $self->throw_exception("SQL::Translator 0.09 required");
201   }
202
203   my $db_tr = SQL::Translator->new({ 
204                                     add_drop_table => 1, 
205                                     parser => 'DBI',
206                                     parser_args => { dbh => $self->storage->dbh }
207                                    });
208
209   $db_tr->producer($db);
210   my $dbic_tr = SQL::Translator->new;
211   $dbic_tr->parser('SQL::Translator::Parser::DBIx::Class');
212   $dbic_tr = $self->storage->configure_sqlt($dbic_tr, $db);
213   $dbic_tr->data($self);
214   $dbic_tr->producer($db);
215
216   $db_tr->schema->name('db_schema');
217   $dbic_tr->schema->name('dbic_schema');
218
219   # is this really necessary?
220   foreach my $tr ($db_tr, $dbic_tr) {
221     my $data = $tr->data;
222     $tr->parser->($tr, $$data);
223   }
224
225   my $diff = SQL::Translator::Diff::schema_diff($db_tr->schema, $db, 
226                                                 $dbic_tr->schema, $db,
227                                                 { ignore_constraint_names => 1, ignore_index_names => 1, caseopt => 1 });
228
229   my $filename = $self->ddl_filename(
230                                          $db,
231                                          $self->schema_version,
232                                          $self->upgrade_directory,
233                                          'PRE',
234                                     );
235   my $file;
236   if(!open($file, ">$filename"))
237     {
238       $self->throw_exception("Can't open $filename for writing ($!)");
239       next;
240     }
241   print $file $diff;
242   close($file);
243
244   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";
245 }
246
247 =head2 upgrade
248
249 Call this to attempt to upgrade your database from the version it is at to the version
250 this DBIC schema is at. 
251
252 It requires an SQL diff file to exist in $schema->upgrade_directory, normally you will
253 have created this using $schema->create_ddl_dir.
254
255 =cut
256
257 sub upgrade
258 {
259   my ($self) = @_;
260   my $db_version = $self->get_db_version();
261
262   # db unversioned
263   unless ($db_version) {
264     # set version in dbix_class_schema_versions table, can't actually upgrade as we don 't know what version the DB is at
265     $self->_create_db_to_schema_diff() if ($self->do_diff_on_init);
266
267     # create versions table and version row
268     $self->{vschema}->deploy;
269     $self->_set_db_version;
270     return;
271   }
272
273   # db and schema at same version. do nothing
274   if ($db_version eq $self->schema_version) {
275     print "Upgrade not necessary\n";
276     return;
277   }
278
279   # strangely the first time this is called can
280   # differ to subsequent times. so we call it 
281   # here to be sure.
282   # XXX - just fix it
283   $self->storage->sqlt_type;
284   
285   my $upgrade_file = $self->ddl_filename(
286                                          $self->storage->sqlt_type,
287                                          $self->schema_version,
288                                          $self->upgrade_directory,
289                                          $db_version,
290                                         );
291
292   unless (-f $upgrade_file) {
293     warn "Upgrade not possible, no upgrade file found ($upgrade_file), please create one\n";
294     return;
295   }
296
297   # backup if necessary then apply upgrade
298   $self->_filedata($self->_read_sql_file($upgrade_file));
299   $self->backup() if($self->do_backup);
300   $self->txn_do(sub { $self->do_upgrade() });
301
302   # set row in dbix_class_schema_versions table
303   $self->_set_db_version;
304 }
305
306 sub _set_db_version {
307   my $self = shift;
308
309   my $vtable = $self->{vschema}->resultset('Table');
310   $vtable->create({ version => $self->schema_version,
311                       installed => strftime("%Y-%m-%d %H:%M:%S", gmtime())
312                       });
313
314 }
315
316 sub _read_sql_file {
317   my $self = shift;
318   my $file = shift || return;
319
320   my $fh;
321   open $fh, "<$file" or warn("Can't open upgrade file, $file ($!)");
322   my @data = split(/\n/, join('', <$fh>));
323   @data = grep(!/^--/, @data);
324   @data = split(/;/, join('', @data));
325   close($fh);
326   @data = grep { $_ && $_ !~ /^-- / } @data;
327   @data = grep { $_ !~ /^(BEGIN TRANSACTION|COMMIT)/m } @data;
328   return \@data;
329 }
330
331 =head2 do_upgrade
332
333 This is an overwritable method used to run your upgrade. The freeform method
334 allows you to run your upgrade any way you please, you can call C<run_upgrade>
335 any number of times to run the actual SQL commands, and in between you can
336 sandwich your data upgrading. For example, first run all the B<CREATE>
337 commands, then migrate your data from old to new tables/formats, then 
338 issue the DROP commands when you are finished. Will run the whole file as it is by default.
339
340 =cut
341
342 sub do_upgrade
343 {
344   my ($self) = @_;
345
346   # just run all the commands (including inserts) in order                                                        
347   $self->run_upgrade(qr/.*?/);
348 }
349
350 =head2 run_upgrade
351
352  $self->run_upgrade(qr/create/i);
353
354 Runs a set of SQL statements matching a passed in regular expression. The
355 idea is that this method can be called any number of times from your
356 C<upgrade> method, running whichever commands you specify via the
357 regex in the parameter. Probably won't work unless called from the overridable
358 do_upgrade method.
359
360 =cut
361
362 sub run_upgrade
363 {
364     my ($self, $stm) = @_;
365
366     return unless ($self->_filedata);
367     my @statements = grep { $_ =~ $stm } @{$self->_filedata};
368     $self->_filedata([ grep { $_ !~ /$stm/i } @{$self->_filedata} ]);
369
370     for (@statements)
371     {      
372         $self->storage->debugobj->query_start($_) if $self->storage->debug;
373         $self->storage->dbh->do($_) or warn "SQL was:\n $_";
374         $self->storage->debugobj->query_end($_) if $self->storage->debug;
375     }
376
377     return 1;
378 }
379
380 =head2 connection
381
382 Overloaded method. This checks the DBIC schema version against the DB version and
383 warns if they are not the same or if the DB is unversioned. It also provides
384 compatibility between the old versions table (SchemaVersions) and the new one
385 (dbix_class_schema_versions).
386
387 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:
388
389   my $schema = MyApp::Schema->connect(
390     $dsn,
391     $user,
392     $password,
393     { ignore_version => 1 },
394   );
395
396 =cut
397
398 sub connection {
399   my $self = shift;
400   $self->next::method(@_);
401   $self->_on_connect($_[3]);
402   return $self;
403 }
404
405 sub _on_connect
406 {
407   my ($self, $args) = @_;
408
409   $args = {} unless $args;
410   $self->{vschema} = DBIx::Class::Version->connect(@{$self->storage->connect_info()});
411   my $vtable = $self->{vschema}->resultset('Table');
412
413   # check for legacy versions table and move to new if exists
414   my $vschema_compat = DBIx::Class::VersionCompat->connect(@{$self->storage->connect_info()});
415   unless ($self->_source_exists($vtable)) {
416     my $vtable_compat = $vschema_compat->resultset('TableCompat');
417     if ($self->_source_exists($vtable_compat)) {
418       $self->{vschema}->deploy;
419       map { $vtable->create({ installed => $_->Installed, version => $_->Version }) } $vtable_compat->all;
420       $self->storage->dbh->do("DROP TABLE " . $vtable_compat->result_source->from);
421     }
422   }
423
424   # useful when connecting from scripts etc
425   return if ($args->{ignore_version} || ($ENV{DBIC_NO_VERSION_CHECK} && !exists $args->{ignore_version}));
426   my $pversion = $self->get_db_version();
427
428   if($pversion eq $self->schema_version)
429     {
430 #         warn "This version is already installed\n";
431         return 1;
432     }
433
434   if(!$pversion)
435     {
436         warn "Your DB is currently unversioned. Please call upgrade on your schema to sync the DB.\n";
437         return 1;
438     }
439
440   warn "Versions out of sync. This is " . $self->schema_version . 
441     ", your database contains version $pversion, please call upgrade on your Schema.\n";
442 }
443
444 1;
445
446
447 =head1 AUTHORS
448
449 Jess Robinson <castaway@desert-island.demon.co.uk>
450 Luke Saunders <luke@shadowcatsystems.co.uk>
451
452 =head1 LICENSE
453
454 You may distribute this code under the same terms as Perl itself.