Version Table Result
[dbsrgits/DBIx-Class-DeploymentHandler.git] / lib / DBIx / Class / DeploymentHandler.pm
CommitLineData
b974984a 1package DBIx::Class::DeploymentHandler;
2
3use Moose;
4use Method::Signatures::Simple;
5require DBIx::Class::Schema; # loaded for type constraint
6require DBIx::Class::Storage; # loaded for type constraint
7use Carp 'carp';
8
9has schema => (
10 isa => 'DBIx::Class::Schema',
11 is => 'ro',
12 required => 1,
13);
14
15has upgrade_directory => (
16 isa => 'Str',
17 is => 'ro',
18 required => 1,
19 default => 'sql',
20);
21
22has backup_directory => (
23 isa => 'Str',
24 is => 'ro',
25);
26
27has storage => (
28 isa => 'DBIx::Class::Storage',
29 is => 'ro',
30 lazy_build => 1,
31);
32
33has _filedata => (
34 is => 'ro',
35);
36
37has do_backup => (
38 isa => 'Bool',
39 is => 'ro',
40 default => undef,
41);
42
43has do_diff_on_init => (
44 isa => 'Bool',
45 is => 'ro',
46 default => undef,
47);
48
49method _build_storage {
50 return $self->schema->storage;
51}
52
53method install($new_version) {
54 # must be called on a fresh database
55 if ($self->get_db_version) {
56 carp 'Install not possible as versions table already exists in database';
57 }
58
59 # default to current version if none passed
60 $new_version ||= $self->schema_version();
61
62 if ($new_version) {
63 # create versions table and version row
64 $self->{vschema}->deploy;
65 $self->_set_db_version({ version => $new_version });
66 }
67}
68
69method deploy {
70 $self->next::method(@_);
71 $self->install();
72}
73
74sub create_upgrade_path {
75 ## override this method
76}
77
78sub ordered_schema_versions {
79 ## override this method
80}
81
82method upgrade {
83 my $db_version = $self->get_db_version();
84
85 # db unversioned
86 unless ($db_version) {
87 carp 'Upgrade not possible as database is unversioned. Please call install first.';
88 return;
89 }
90
91 # db and schema at same version. do nothing
92 if ( $db_version eq $self->schema_version ) {
93 carp "Upgrade not necessary\n";
94 return;
95 }
96
97 my @version_list = $self->ordered_schema_versions;
98
99 # if nothing returned then we preload with min/max
100 @version_list = ( $db_version, $self->schema_version )
101 unless ( scalar(@version_list) );
102
103 # catch the case of someone returning an arrayref
104 @version_list = @{ $version_list[0] }
105 if ( ref( $version_list[0] ) eq 'ARRAY' );
106
107 # remove all versions in list above the required version
108 while ( scalar(@version_list)
109 && ( $version_list[-1] ne $self->schema_version ) )
110 {
111 pop @version_list;
112 }
113
114 # remove all versions in list below the current version
115 while ( scalar(@version_list) && ( $version_list[0] ne $db_version ) ) {
116 shift @version_list;
117 }
118
119 # check we have an appropriate list of versions
120 if ( scalar(@version_list) < 2 ) {
121 die;
122 }
123
124 # do sets of upgrade
125 while ( scalar(@version_list) >= 2 ) {
126 $self->upgrade_single_step( $version_list[0], $version_list[1] );
127 shift @version_list;
128 }
129}
130
131method upgrade_single_step($db_version, $target_version) {
132 # db and schema at same version. do nothing
133 if ($db_version eq $target_version) {
134 carp "Upgrade not necessary\n";
135 return;
136 }
137
138 # strangely the first time this is called can
139 # differ to subsequent times. so we call it
140 # here to be sure.
141 # XXX - just fix it
142 $self->storage->sqlt_type;
143
144 my $upgrade_file = $self->ddl_filename(
145 $self->storage->sqlt_type,
146 $target_version,
147 $self->upgrade_directory,
148 $db_version,
149 );
150
151 $self->create_upgrade_path({ upgrade_file => $upgrade_file });
152
153 unless (-f $upgrade_file) {
154 carp "Upgrade not possible, no upgrade file found ($upgrade_file), please create one\n";
155 return;
156 }
157
158 carp "DB version ($db_version) is lower than the schema version (".$self->schema_version."). Attempting upgrade.\n";
159
160 # backup if necessary then apply upgrade
161 $self->_filedata($self->_read_sql_file($upgrade_file));
162 $self->backup() if($self->do_backup);
163 $self->txn_do(sub { $self->do_upgrade() });
164
165 # set row in dbix_class_schema_versions table
166 $self->_set_db_version({version => $target_version});
167}
168
169method do_upgrade {
170 # just run all the commands (including inserts) in order
171 $self->run_upgrade(qr/.*?/);
172}
173
174method run_upgrade($stm) {
175 return unless ($self->_filedata);
176 my @statements = grep { $_ =~ $stm } @{$self->_filedata};
177 $self->_filedata([ grep { $_ !~ /$stm/i } @{$self->_filedata} ]);
178
179 for (@statements) {
180 $self->storage->debugobj->query_start($_) if $self->storage->debug;
181 $self->apply_statement($_);
182 $self->storage->debugobj->query_end($_) if $self->storage->debug;
183 }
184
185 return 1;
186}
187
188method apply_statement($statement) {
189 $self->storage->dbh->do($_) or carp "SQL was: $_";
190}
191
192method get_db_version($rs) {
193 my $vtable = $self->{vschema}->resultset('Table');
194 my $version = eval {
195 $vtable->search({}, { order_by => { -desc => 'installed' }, rows => 1 } )
196 ->get_column ('version')
197 ->next;
198 };
199 return $version || 0;
200}
201
202method schema_version {}
203
204method backup {
205 ## Make each ::DBI::Foo do this
206 $self->storage->backup($self->backup_directory());
207}
208
209method connection {
210 $self->next::method(@_);
211 $self->_on_connect($_[3]);
212 return $self;
213}
214
215sub _on_connect
216{
217 my ($self, $args) = @_;
218
219 $args = {} unless $args;
220
221 $self->{vschema} = DBIx::Class::Version->connect(@{$self->storage->connect_info()});
222 my $vtable = $self->{vschema}->resultset('Table');
223
224 # useful when connecting from scripts etc
225 return if ($args->{ignore_version} || ($ENV{DBIC_NO_VERSION_CHECK} && !exists $args->{ignore_version}));
226
227 # check for legacy versions table and move to new if exists
228 my $vschema_compat = DBIx::Class::VersionCompat->connect(@{$self->storage->connect_info()});
229 unless ($self->_source_exists($vtable)) {
230 my $vtable_compat = $vschema_compat->resultset('TableCompat');
231 if ($self->_source_exists($vtable_compat)) {
232 $self->{vschema}->deploy;
233 map { $vtable->create({ installed => $_->Installed, version => $_->Version }) } $vtable_compat->all;
234 $self->storage->dbh->do("DROP TABLE " . $vtable_compat->result_source->from);
235 }
236 }
237
238 my $pversion = $self->get_db_version();
239
240 if($pversion eq $self->schema_version)
241 {
242# carp "This version is already installed\n";
243 return 1;
244 }
245
246 if(!$pversion)
247 {
248 carp "Your DB is currently unversioned. Please call upgrade on your schema to sync the DB.\n";
249 return 1;
250 }
251
252 carp "Versions out of sync. This is " . $self->schema_version .
253 ", your database contains version $pversion, please call upgrade on your Schema.\n";
254}
255
256sub _create_db_to_schema_diff {
257 my $self = shift;
258
259 my %driver_to_db_map = (
260 'mysql' => 'MySQL'
261 );
262
263 my $db = $driver_to_db_map{$self->storage->dbh->{Driver}->{Name}};
264 unless ($db) {
265 print "Sorry, this is an unsupported DB\n";
266 return;
267 }
268
269 $self->throw_exception($self->storage->_sqlt_version_error)
270 if (not $self->storage->_sqlt_version_ok);
271
272 my $db_tr = SQL::Translator->new({
273 add_drop_table => 1,
274 parser => 'DBI',
275 parser_args => { dbh => $self->storage->dbh }
276 });
277
278 $db_tr->producer($db);
279 my $dbic_tr = SQL::Translator->new;
280 $dbic_tr->parser('SQL::Translator::Parser::DBIx::Class');
281 $dbic_tr->data($self);
282 $dbic_tr->producer($db);
283
284 $db_tr->schema->name('db_schema');
285 $dbic_tr->schema->name('dbic_schema');
286
287 # is this really necessary?
288 foreach my $tr ($db_tr, $dbic_tr) {
289 my $data = $tr->data;
290 $tr->parser->($tr, $$data);
291 }
292
293 my $diff = SQL::Translator::Diff::schema_diff($db_tr->schema, $db,
294 $dbic_tr->schema, $db,
295 { ignore_constraint_names => 1, ignore_index_names => 1, caseopt => 1 });
296
297 my $filename = $self->ddl_filename(
298 $db,
299 $self->schema_version,
300 $self->upgrade_directory,
301 'PRE',
302 );
303 my $file;
304 if(!open($file, ">$filename"))
305 {
306 $self->throw_exception("Can't open $filename for writing ($!)");
307 next;
308 }
309 print $file $diff;
310 close($file);
311
312 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";
313}
314
315
316sub _set_db_version {
317 my $self = shift;
318 my ($params) = @_;
319 $params ||= {};
320
321 my $version = $params->{version} ? $params->{version} : $self->schema_version;
322 my $vtable = $self->{vschema}->resultset('Table');
323
324 ##############################################################################
325 # !!! NOTE !!!
326 ##############################################################################
327 #
328 # The travesty below replaces the old nice timestamp format of %Y-%m-%d %H:%M:%S
329 # This is necessary since there are legitimate cases when upgrades can happen
330 # back to back within the same second. This breaks things since we relay on the
331 # ability to sort by the 'installed' value. The logical choice of an autoinc
332 # is not possible, as it will break multiple legacy installations. Also it is
333 # not possible to format the string sanely, as the column is a varchar(20).
334 # The 'v' character is added to the front of the string, so that any version
335 # formatted by this new function will sort _after_ any existing 200... strings.
336 my @tm = gettimeofday();
337 my @dt = gmtime ($tm[0]);
338 my $o = $vtable->create({
339 version => $version,
340 installed => sprintf("v%04d%02d%02d_%02d%02d%02d.%03.0f",
341 $dt[5] + 1900,
342 $dt[4] + 1,
343 $dt[3],
344 $dt[2],
345 $dt[1],
346 $dt[0],
347 $tm[1] / 1000, # convert to millisecs, format as up/down rounded int above
348 ),
349 });
350}
351
352sub _read_sql_file {
353 my $self = shift;
354 my $file = shift || return;
355
356 open my $fh, '<', $file or carp("Can't open upgrade file, $file ($!)");
357 my @data = split /\n/, join '', <$fh>;
358 close $fh;
359
360 @data = grep {
361 $_ &&
362 !/^--/ &&
363 !/^(BEGIN|BEGIN TRANSACTION|COMMIT)/m
364 } split /;/,
365 join '', @data;
366
367 return \@data;
368}
369
370sub _source_exists
371{
372 my ($self, $rs) = @_;
373
374 my $c = eval {
375 $rs->search({ 1, 0 })->count;
376 };
377 return 0 if $@ || !defined $c;
378
379 return 1;
380}
381
3821;