initial pod docs
[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   default => sub { [] },
63 );
64
65 has txn_wrap => (
66   is => 'ro',
67   isa => 'Bool',
68   default => 1,
69 );
70
71 method __ddl_consume_with_prefix($type, $versions, $prefix) {
72   my $base_dir = $self->upgrade_directory;
73
74   my $main    = catfile( $base_dir, $type      );
75   my $generic = catfile( $base_dir, '_generic' );
76   my $common  =
77     catfile( $base_dir, '_common', $prefix, join q(-), @{$versions} );
78
79   my $dir;
80   if (-d $main) {
81     $dir = catfile($main, $prefix, join q(-), @{$versions})
82   } elsif (-d $generic) {
83     $dir = catfile($generic, $prefix, join q(-), @{$versions});
84   } else {
85     croak "neither $main or $generic exist; please write/generate some SQL";
86   }
87
88   opendir my($dh), $dir;
89   my %files = map { $_ => "$dir/$_" } grep { /\.(?:sql|pl)$/ && -f "$dir/$_" } readdir $dh;
90   closedir $dh;
91
92   if (-d $common) {
93     opendir my($dh), $common;
94     for my $filename (grep { /\.(?:sql|pl)$/ && -f catfile($common,$_) } readdir $dh) {
95       unless ($files{$filename}) {
96         $files{$filename} = catfile($common,$filename);
97       }
98     }
99     closedir $dh;
100   }
101
102   return [@files{sort keys %files}]
103 }
104
105 method _ddl_schema_consume_filenames($type, $version) {
106   $self->__ddl_consume_with_prefix($type, [ $version ], 'schema')
107 }
108
109 method _ddl_schema_produce_filename($type, $version) {
110   my $dirname = catfile( $self->upgrade_directory, $type, 'schema', $version );
111   mkpath($dirname) unless -d $dirname;
112
113   return catfile( $dirname, '001-auto.sql' );
114 }
115
116 method _ddl_schema_up_consume_filenames($type, $versions) {
117   $self->__ddl_consume_with_prefix($type, $versions, 'up')
118 }
119
120 method _ddl_schema_down_consume_filenames($type, $versions) {
121   $self->__ddl_consume_with_prefix($type, $versions, 'down')
122 }
123
124 method _ddl_schema_up_produce_filename($type, $versions) {
125   my $dir = $self->upgrade_directory;
126
127   my $dirname = catfile( $dir, $type, 'up', join q(-), @{$versions});
128   mkpath($dirname) unless -d $dirname;
129
130   return catfile( $dirname, '001-auto.sql'
131   );
132 }
133
134 method _ddl_schema_down_produce_filename($type, $versions, $dir) {
135   my $dirname = catfile( $dir, $type, 'down', join q(-), @{$versions} );
136   mkpath($dirname) unless -d $dirname;
137
138   return catfile( $dirname, '001-auto.sql');
139 }
140
141 method _run_sql_and_perl($filenames) {
142   my @files = @{$filenames};
143   my $storage = $self->storage;
144
145
146   my $guard = $self->schema->txn_scope_guard if $self->txn_wrap;
147
148   my $sql;
149   for my $filename (@files) {
150     if ($filename =~ /\.sql$/) {
151       my @sql = @{$self->_read_sql_file($filename)};
152       $sql .= join "\n", @sql;
153
154       foreach my $line (@sql) {
155         $storage->_query_start($line);
156         try {
157           # do a dbh_do cycle here, as we need some error checking in
158           # place (even though we will ignore errors)
159           $storage->dbh_do (sub { $_[1]->do($line) });
160         }
161         catch {
162           carp "$_ (running '${line}')"
163         }
164         $storage->_query_end($line);
165       }
166     } elsif ( $filename =~ /^(.+)\.pl$/ ) {
167       my $package = $1;
168       my $filedata = do { local( @ARGV, $/ ) = $filename; <> };
169       # make the package name more palateable to perl
170       $package =~ s/\W/_/g;
171
172       no warnings 'redefine';
173       eval "package $package;\n\n$filedata";
174       use warnings;
175
176       if (my $fn = $package->can('run')) {
177         $fn->($self->schema);
178       } else {
179         carp "$filename should define a run method that takes a schema but it didn't!";
180       }
181     } else {
182       croak "A file got to deploy that wasn't sql or perl!";
183     }
184   }
185
186   $guard->commit if $self->txn_wrap;
187
188   return $sql;
189 }
190
191 sub deploy {
192   my $self = shift;
193
194   return $self->_run_sql_and_perl($self->_ddl_schema_consume_filenames(
195     $self->storage->sqlt_type,
196     $self->schema_version
197   ));
198 }
199
200 sub _prepare_install {
201   my $self = shift;
202   my $sqltargs  = { %{$self->sqltargs}, %{shift @_} };
203   my $to_file   = shift;
204   my $schema    = $self->schema;
205   my $databases = $self->databases;
206   my $dir       = $self->upgrade_directory;
207   my $version = $schema->schema_version;
208
209   my $sqlt = SQL::Translator->new({
210     add_drop_table          => 1,
211     ignore_constraint_names => 1,
212     ignore_index_names      => 1,
213     parser                  => 'SQL::Translator::Parser::DBIx::Class',
214     %{$sqltargs}
215   });
216
217   my $sqlt_schema = $sqlt->translate( data => $schema )
218     or croak($sqlt->error);
219
220   foreach my $db (@$databases) {
221     $sqlt->reset;
222     $sqlt->{schema} = $sqlt_schema;
223     $sqlt->producer($db);
224
225     my $filename = $self->$to_file($db, $version, $dir);
226     if (-e $filename ) {
227       carp "Overwriting existing DDL file - $filename";
228       unlink $filename;
229     }
230
231     my $output = $sqlt->translate;
232     if(!$output) {
233       carp("Failed to translate to $db, skipping. (" . $sqlt->error . ")");
234       next;
235     }
236     open my $file, q(>), $filename;
237     print {$file} $output;
238     close $file;
239   }
240 }
241
242 sub _resultsource_install_filename {
243   my ($self, $source_name) = @_;
244   return sub {
245     my ($self, $type, $version) = @_;
246     my $dirname = catfile( $self->upgrade_directory, $type, 'schema', $version );
247     mkpath($dirname) unless -d $dirname;
248
249     return catfile( $dirname, "001-auto-$source_name.sql" );
250   }
251 }
252
253 sub install_resultsource {
254   my ($self, $source, $version) = @_;
255
256   my $rs_install_file =
257     $self->_resultsource_install_filename($source->source_name);
258
259   my $files = [
260      $self->$rs_install_file(
261       $self->storage->sqlt_type,
262       $version,
263     )
264   ];
265   $self->_run_sql_and_perl($files);
266 }
267
268 sub prepare_resultsource_install {
269   my $self = shift;
270   my $source = shift;
271
272   my $filename = $self->_resultsource_install_filename($source->source_name);
273   $self->_prepare_install({
274       parser_args => { sources => [$source->source_name], }
275     }, $filename);
276 }
277
278 sub prepare_install {
279   my $self = shift;
280   $self->_prepare_install({}, '_ddl_schema_produce_filename');
281 }
282
283 sub prepare_upgrade {
284   my ($self, $from_version, $to_version, $version_set) = @_;
285   $self->_prepare_changegrade($from_version, $to_version, $version_set, 'up');
286 }
287
288 sub prepare_downgrade {
289   my ($self, $from_version, $to_version, $version_set) = @_;
290
291   $self->_prepare_changegrade($from_version, $to_version, $version_set, 'down');
292 }
293
294 method _prepare_changegrade($from_version, $to_version, $version_set, $direction) {
295   my $schema    = $self->schema;
296   my $databases = $self->databases;
297   my $dir       = $self->upgrade_directory;
298   my $sqltargs  = $self->sqltargs;
299
300   my $schema_version = $schema->schema_version;
301
302   $sqltargs = {
303     add_drop_table => 1,
304     ignore_constraint_names => 1,
305     ignore_index_names => 1,
306     %{$sqltargs}
307   };
308
309   my $sqlt = SQL::Translator->new( $sqltargs );
310
311   $sqlt->parser('SQL::Translator::Parser::DBIx::Class');
312   my $sqlt_schema = $sqlt->translate( data => $schema )
313     or croak($sqlt->error);
314
315   foreach my $db (@$databases) {
316     $sqlt->reset;
317     $sqlt->{schema} = $sqlt_schema;
318     $sqlt->producer($db);
319
320     my $prefilename = $self->_ddl_schema_produce_filename($db, $from_version, $dir);
321     unless(-e $prefilename) {
322       carp("No previous schema file found ($prefilename)");
323       next;
324     }
325     my $diff_file_method = "_ddl_schema_${direction}_produce_filename";
326     my $diff_file = $self->$diff_file_method($db, $version_set, $dir );
327     if(-e $diff_file) {
328       carp("Overwriting existing $direction-diff file - $diff_file");
329       unlink $diff_file;
330     }
331
332     my $source_schema;
333     {
334       my $t = SQL::Translator->new({
335          %{$sqltargs},
336          debug => 0,
337          trace => 0,
338       });
339
340       $t->parser( $db ) # could this really throw an exception?
341         or croak($t->error);
342
343       my $out = $t->translate( $prefilename )
344         or croak($t->error);
345
346       $source_schema = $t->schema;
347
348       $source_schema->name( $prefilename )
349         unless  $source_schema->name;
350     }
351
352     # The "new" style of producers have sane normalization and can support
353     # diffing a SQL file against a DBIC->SQLT schema. Old style ones don't
354     # And we have to diff parsed SQL against parsed SQL.
355     my $dest_schema = $sqlt_schema;
356
357     unless ( "SQL::Translator::Producer::$db"->can('preprocess_schema') ) {
358       my $t = SQL::Translator->new({
359          %{$sqltargs},
360          debug => 0,
361          trace => 0,
362       });
363
364       $t->parser( $db ) # could this really throw an exception?
365         or croak($t->error);
366
367       my $filename = $self->_ddl_schema_produce_filename($db, $to_version, $dir);
368       my $out = $t->translate( $filename )
369         or croak($t->error);
370
371       $dest_schema = $t->schema;
372
373       $dest_schema->name( $filename )
374         unless $dest_schema->name;
375     }
376
377     my $diff = SQL::Translator::Diff::schema_diff(
378        $source_schema, $db,
379        $dest_schema,   $db,
380        $sqltargs
381     );
382     open my $file, q(>), $diff_file;
383     print {$file} $diff;
384     close $file;
385   }
386 }
387
388 method _read_sql_file($file) {
389   return unless $file;
390
391   open my $fh, '<', $file;
392   my @data = split /;\n/, join '', <$fh>;
393   close $fh;
394
395   @data = grep {
396     $_ && # remove blank lines
397     !/^(BEGIN|BEGIN TRANSACTION|COMMIT)/ # strip txn's
398   } map {
399     s/^\s+//; s/\s+$//; # trim whitespace
400     join '', grep { !/^--/ } split /\n/ # remove comments
401   } @data;
402
403   return \@data;
404 }
405
406 sub downgrade_single_step {
407   my $self = shift;
408   my $version_set = shift @_;
409
410   my $sql = $self->_run_sql_and_perl($self->_ddl_schema_down_consume_filenames(
411     $self->storage->sqlt_type,
412     $version_set,
413   ));
414
415   return ['', $sql];
416 }
417
418 sub upgrade_single_step {
419   my $self = shift;
420   my $version_set = shift @_;
421
422   my $sql = $self->_run_sql_and_perl($self->_ddl_schema_up_consume_filenames(
423     $self->storage->sqlt_type,
424     $version_set,
425   ));
426   return ['', $sql];
427 }
428
429 __PACKAGE__->meta->make_immutable;
430
431 1;
432
433 __END__
434
435 vim: ts=2 sw=2 expandtab