rearrange use to make more sense
[dbsrgits/DBIx-Class-DeploymentHandler.git] / lib / DBIx / Class / DeploymentHandler / DeployMethod / SQL / Translator.pm
1 package DBIx::Class::DeploymentHandler::DeployMethod::SQL::Translator;
2 use Moose;
3
4 use autodie;
5 use Carp qw( carp croak );
6
7 use Method::Signatures::Simple;
8 use Try::Tiny;
9
10 use SQL::Translator;
11 require SQL::Translator::Diff;
12
13 require DBIx::Class::Storage;   # loaded for type constraint
14 use DBIx::Class::DeploymentHandler::Types;
15
16 use File::Path 'mkpath';
17 use File::Spec::Functions;
18
19 with 'DBIx::Class::DeploymentHandler::HandlesDeploy';
20
21 has schema => (
22   isa      => 'DBIx::Class::Schema',
23   is       => 'ro',
24   required => 1,
25   handles => [qw( schema_version )],
26 );
27
28 has storage => (
29   isa        => 'DBIx::Class::Storage',
30   is         => 'ro',
31   lazy_build => 1,
32 );
33
34 method _build_storage {
35   my $s = $self->schema->storage;
36   $s->_determine_driver;
37   $s
38 }
39
40 has sqltargs => (
41   isa => 'HashRef',
42   is  => 'ro',
43   default => sub { {} },
44 );
45 has upgrade_directory => (
46   isa      => 'Str',
47   is       => 'ro',
48   required => 1,
49   default  => 'sql',
50 );
51
52 has databases => (
53   coerce  => 1,
54   isa     => 'DBIx::Class::DeploymentHandler::Databases',
55   is      => 'ro',
56   default => sub { [qw( MySQL SQLite PostgreSQL )] },
57 );
58
59 has _filedata => (
60   isa => 'ArrayRef[Str]',
61   is  => 'rw',
62 );
63
64 has txn_wrap => (
65   is => 'ro',
66   isa => 'Bool',
67   default => 1,
68 );
69
70 method __ddl_consume_with_prefix($type, $versions, $prefix) {
71   my $base_dir = $self->upgrade_directory;
72
73   my $main    = catfile( $base_dir, $type      );
74   my $generic = catfile( $base_dir, '_generic' );
75   my $common  =
76     catfile( $base_dir, '_common', $prefix, join q(-), @{$versions} );
77
78   my $dir;
79   if (-d $main) {
80     $dir = catfile($main, $prefix, join q(-), @{$versions})
81   } elsif (-d $generic) {
82     $dir = catfile($generic, $prefix, join q(-), @{$versions});
83   } else {
84     croak "neither $main or $generic exist; please write/generate some SQL";
85   }
86
87   opendir my($dh), $dir;
88   my %files = map { $_ => "$dir/$_" } grep { /\.sql$/ && -f "$dir/$_" } readdir $dh;
89   closedir $dh;
90
91   if (-d $common) {
92     opendir my($dh), $common;
93     for my $filename (grep { /\.sql$/ && -f catfile($common,$_) } readdir $dh) {
94       unless ($files{$filename}) {
95         $files{$filename} = catfile($common,$filename);
96       }
97     }
98     closedir $dh;
99   }
100
101   return [@files{sort keys %files}]
102 }
103
104 method _ddl_schema_consume_filenames($type, $version) {
105   $self->__ddl_consume_with_prefix($type, [ $version ], 'schema')
106 }
107
108 method _ddl_schema_produce_filename($type, $version) {
109   my $dirname = catfile( $self->upgrade_directory, $type, 'schema', $version );
110   mkpath($dirname) unless -d $dirname;
111
112   return catfile( $dirname, '001-auto.sql' );
113 }
114
115 method _ddl_schema_up_consume_filenames($type, $versions) {
116   $self->__ddl_consume_with_prefix($type, $versions, 'up')
117 }
118
119 method _ddl_schema_down_consume_filenames($type, $versions) {
120   $self->__ddl_consume_with_prefix($type, $versions, 'down')
121 }
122
123 method _ddl_schema_up_produce_filename($type, $versions) {
124   my $dir = $self->upgrade_directory;
125
126   my $dirname = catfile( $dir, $type, 'up', join q(-), @{$versions});
127   mkpath($dirname) unless -d $dirname;
128
129   return catfile( $dirname, '001-auto.sql'
130   );
131 }
132
133 method _ddl_schema_down_produce_filename($type, $versions, $dir) {
134   my $dirname = catfile( $dir, $type, 'down', join q(-), @{$versions} );
135   mkpath($dirname) unless -d $dirname;
136
137   return catfile( $dirname, '001-auto.sql');
138 }
139
140 sub _deploy {
141   my $self = shift;
142   my $storage  = $self->storage;
143
144   my $guard = $self->schema->txn_scope_guard if $self->txn_wrap;
145
146   my @sql = map @{$self->_read_sql_file($_)}, @{$self->_ddl_schema_consume_filenames(
147       $self->storage->sqlt_type,
148       $self->schema_version
149     )};
150
151   foreach my $line (@sql) {
152     $storage->_query_start($line);
153     try {
154       # do a dbh_do cycle here, as we need some error checking in
155       # place (even though we will ignore errors)
156       $storage->dbh_do (sub { $_[1]->do($line) });
157     }
158     catch {
159       carp "$_ (running '${line}')"
160     }
161     $storage->_query_end($line);
162   }
163
164   $guard->commit if $self->txn_wrap;
165   return join "\n", @sql;
166 }
167
168 sub prepare_install {
169   my $self = shift;
170   my $schema    = $self->schema;
171   my $databases = $self->databases;
172   my $dir       = $self->upgrade_directory;
173   my $sqltargs  = $self->sqltargs;
174   my $version = $schema->schema_version;
175
176   my $sqlt = SQL::Translator->new({
177     add_drop_table          => 1,
178     ignore_constraint_names => 1,
179     ignore_index_names      => 1,
180     parser                  => 'SQL::Translator::Parser::DBIx::Class',
181     %{$sqltargs}
182   });
183
184   my $sqlt_schema = $sqlt->translate( data => $schema )
185     or $self->throw_exception($sqlt->error);
186
187   foreach my $db (@$databases) {
188     $sqlt->reset;
189     $sqlt->{schema} = $sqlt_schema;
190     $sqlt->producer($db);
191
192     my $filename = $self->_ddl_schema_produce_filename($db, $version, $dir);
193     if (-e $filename ) {
194       carp "Overwriting existing DDL file - $filename";
195       unlink $filename;
196     }
197
198     my $output = $sqlt->translate;
199     if(!$output) {
200       carp("Failed to translate to $db, skipping. (" . $sqlt->error . ")");
201       next;
202     }
203     my $file;
204     unless( open $file, q(>), $filename ) {
205       $self->throw_exception("Can't open $filename for writing ($!)");
206       next;
207     }
208     print {$file} $output;
209     close $file;
210   }
211 }
212
213 sub prepare_upgrade {
214   my ($self, $from_version, $to_version, $version_set) = @_;
215
216   $from_version ||= '1.0'; #$self->database_version;
217   $to_version   ||= $self->schema_version;
218
219   # for updates prepared automatically (rob's stuff)
220   # one would want to explicitly set $version_set to
221   # [$to_version]
222   $version_set  ||= [$from_version, $to_version];
223
224   $self->_prepare_changegrade($from_version, $to_version, $version_set, 'up');
225 }
226
227 sub prepare_downgrade {
228   my ($self, $from_version, $to_version, $version_set) = @_;
229
230   $from_version ||= $self->db_version;
231   $to_version   ||= $self->schema_version;
232
233   # for updates prepared automatically (rob's stuff)
234   # one would want to explicitly set $version_set to
235   # [$to_version]
236   $version_set  ||= [$from_version, $to_version];
237
238   $self->_prepare_changegrade($from_version, $to_version, $version_set, 'down');
239 }
240
241 method _prepare_changegrade($from_version, $to_version, $version_set, $direction) {
242   my $schema    = $self->schema;
243   my $databases = $self->databases;
244   my $dir       = $self->upgrade_directory;
245   my $sqltargs  = $self->sqltargs;
246
247   my $schema_version = $schema->schema_version;
248
249   $sqltargs = {
250     add_drop_table => 1,
251     ignore_constraint_names => 1,
252     ignore_index_names => 1,
253     %{$sqltargs}
254   };
255
256   my $sqlt = SQL::Translator->new( $sqltargs );
257
258   $sqlt->parser('SQL::Translator::Parser::DBIx::Class');
259   my $sqlt_schema = $sqlt->translate( data => $schema )
260     or $self->throw_exception ($sqlt->error);
261
262   foreach my $db (@$databases) {
263     $sqlt->reset;
264     $sqlt->{schema} = $sqlt_schema;
265     $sqlt->producer($db);
266
267     my $prefilename = $self->_ddl_schema_produce_filename($db, $from_version, $dir);
268     unless(-e $prefilename) {
269       carp("No previous schema file found ($prefilename)");
270       next;
271     }
272     my $diff_file_method = "_ddl_schema_${direction}_produce_filename";
273     my $diff_file = $self->$diff_file_method($db, $version_set, $dir );
274     if(-e $diff_file) {
275       carp("Overwriting existing $direction-diff file - $diff_file");
276       unlink $diff_file;
277     }
278
279     my $source_schema;
280     {
281       my $t = SQL::Translator->new({
282          %{$sqltargs},
283          debug => 0,
284          trace => 0,
285       });
286
287       $t->parser( $db ) # could this really throw an exception?
288         or $self->throw_exception ($t->error);
289
290       my $out = $t->translate( $prefilename )
291         or $self->throw_exception ($t->error);
292
293       $source_schema = $t->schema;
294
295       $source_schema->name( $prefilename )
296         unless  $source_schema->name;
297     }
298
299     # The "new" style of producers have sane normalization and can support
300     # diffing a SQL file against a DBIC->SQLT schema. Old style ones don't
301     # And we have to diff parsed SQL against parsed SQL.
302     my $dest_schema = $sqlt_schema;
303
304     unless ( "SQL::Translator::Producer::$db"->can('preprocess_schema') ) {
305       my $t = SQL::Translator->new({
306          %{$sqltargs},
307          debug => 0,
308          trace => 0,
309       });
310
311       $t->parser( $db ) # could this really throw an exception?
312         or $self->throw_exception ($t->error);
313
314       my $filename = $self->_ddl_schema_produce_filename($db, $to_version, $dir);
315       my $out = $t->translate( $filename )
316         or $self->throw_exception ($t->error);
317
318       $dest_schema = $t->schema;
319
320       $dest_schema->name( $filename )
321         unless $dest_schema->name;
322     }
323
324     my $diff = SQL::Translator::Diff::schema_diff(
325        $source_schema, $db,
326        $dest_schema,   $db,
327        $sqltargs
328     );
329     my $file;
330     unless(open $file, q(>), $diff_file) {
331       $self->throw_exception("Can't write to $diff_file ($!)");
332       next;
333     }
334     print {$file} $diff;
335     close $file;
336   }
337 }
338
339 method _read_sql_file($file) {
340   return unless $file;
341
342   open my $fh, '<', $file or carp("Can't open sql file, $file ($!)");
343   my @data = split /;\n/, join '', <$fh>;
344   close $fh;
345
346   @data = grep {
347     $_ && # remove blank lines
348     !/^(BEGIN|BEGIN TRANSACTION|COMMIT)/ # strip txn's
349   } map {
350     s/^\s+//; s/\s+$//; # trim whitespace
351     join '', grep { !/^--/ } split /\n/ # remove comments
352   } @data;
353
354   return \@data;
355 }
356
357 sub _downgrade_single_step {
358   my $self = shift;
359   my @version_set = @{ shift @_ };
360   my @downgrade_files = @{$self->_ddl_schema_down_consume_filenames(
361     $self->storage->sqlt_type,
362     \@version_set,
363   )};
364
365   for my $downgrade_file (@downgrade_files) {
366     unless (-f $downgrade_file) {
367       # croak?
368       carp "Downgrade not possible, no downgrade file found ($downgrade_file), please create one\n";
369       return;
370     }
371
372     $self->_filedata($self->_read_sql_file($downgrade_file)); # I don't like this --fREW 2010-02-22
373
374     my $guard = $self->schema->txn_scope_guard if $self->txn_wrap;
375     $self->_do_upgrade;
376     $guard->commit if $self->txn_wrap;
377   }
378 }
379
380 sub _upgrade_single_step {
381   my $self = shift;
382   my @version_set = @{ shift @_ };
383   my @upgrade_files = @{$self->_ddl_schema_up_consume_filenames(
384     $self->storage->sqlt_type,
385     \@version_set,
386   )};
387
388   for my $upgrade_file (@upgrade_files) {
389     unless (-f $upgrade_file) {
390       # croak?
391       carp "Upgrade not possible, no upgrade file found ($upgrade_file), please create one\n";
392       return;
393     }
394
395     $self->_filedata($self->_read_sql_file($upgrade_file)); # I don't like this --fREW 2010-02-22
396     my $guard = $self->schema->txn_scope_guard if $self->txn_wrap;
397     $self->_do_upgrade;
398     $guard->commit if $self->txn_wrap;
399   }
400 }
401
402 method _do_upgrade { $self->_run_upgrade(qr/.*?/) }
403
404 method _run_upgrade($stm) {
405   return unless $self->_filedata;
406   my @statements = grep { $_ =~ $stm } @{$self->_filedata};
407
408   for (@statements) {
409     $self->storage->debugobj->query_start($_) if $self->storage->debug;
410     $self->_apply_statement($_);
411     $self->storage->debugobj->query_end($_) if $self->storage->debug;
412   }
413 }
414
415 method _apply_statement($statement) {
416   # croak?
417   $self->storage->dbh->do($_) or carp "SQL was: $_"
418 }
419
420 1;
421
422 __END__
423
424 vim: ts=2 sw=2 expandtab