initial cut at install_resultsource (and therefore install_version_storage)
[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   # for updates prepared automatically (rob's stuff)
286   # one would want to explicitly set $version_set to
287   # [$to_version]
288   $self->_prepare_changegrade($from_version, $to_version, $version_set, 'up');
289 }
290
291 sub prepare_downgrade {
292   my ($self, $from_version, $to_version, $version_set) = @_;
293
294   # for updates prepared automatically (rob's stuff)
295   # one would want to explicitly set $version_set to
296   # [$to_version]
297   $self->_prepare_changegrade($from_version, $to_version, $version_set, 'down');
298 }
299
300 method _prepare_changegrade($from_version, $to_version, $version_set, $direction) {
301   my $schema    = $self->schema;
302   my $databases = $self->databases;
303   my $dir       = $self->upgrade_directory;
304   my $sqltargs  = $self->sqltargs;
305
306   my $schema_version = $schema->schema_version;
307
308   $sqltargs = {
309     add_drop_table => 1,
310     ignore_constraint_names => 1,
311     ignore_index_names => 1,
312     %{$sqltargs}
313   };
314
315   my $sqlt = SQL::Translator->new( $sqltargs );
316
317   $sqlt->parser('SQL::Translator::Parser::DBIx::Class');
318   my $sqlt_schema = $sqlt->translate( data => $schema )
319     or croak($sqlt->error);
320
321   foreach my $db (@$databases) {
322     $sqlt->reset;
323     $sqlt->{schema} = $sqlt_schema;
324     $sqlt->producer($db);
325
326     my $prefilename = $self->_ddl_schema_produce_filename($db, $from_version, $dir);
327     unless(-e $prefilename) {
328       carp("No previous schema file found ($prefilename)");
329       next;
330     }
331     my $diff_file_method = "_ddl_schema_${direction}_produce_filename";
332     my $diff_file = $self->$diff_file_method($db, $version_set, $dir );
333     if(-e $diff_file) {
334       carp("Overwriting existing $direction-diff file - $diff_file");
335       unlink $diff_file;
336     }
337
338     my $source_schema;
339     {
340       my $t = SQL::Translator->new({
341          %{$sqltargs},
342          debug => 0,
343          trace => 0,
344       });
345
346       $t->parser( $db ) # could this really throw an exception?
347         or croak($t->error);
348
349       my $out = $t->translate( $prefilename )
350         or croak($t->error);
351
352       $source_schema = $t->schema;
353
354       $source_schema->name( $prefilename )
355         unless  $source_schema->name;
356     }
357
358     # The "new" style of producers have sane normalization and can support
359     # diffing a SQL file against a DBIC->SQLT schema. Old style ones don't
360     # And we have to diff parsed SQL against parsed SQL.
361     my $dest_schema = $sqlt_schema;
362
363     unless ( "SQL::Translator::Producer::$db"->can('preprocess_schema') ) {
364       my $t = SQL::Translator->new({
365          %{$sqltargs},
366          debug => 0,
367          trace => 0,
368       });
369
370       $t->parser( $db ) # could this really throw an exception?
371         or croak($t->error);
372
373       my $filename = $self->_ddl_schema_produce_filename($db, $to_version, $dir);
374       my $out = $t->translate( $filename )
375         or croak($t->error);
376
377       $dest_schema = $t->schema;
378
379       $dest_schema->name( $filename )
380         unless $dest_schema->name;
381     }
382
383     my $diff = SQL::Translator::Diff::schema_diff(
384        $source_schema, $db,
385        $dest_schema,   $db,
386        $sqltargs
387     );
388     open my $file, q(>), $diff_file;
389     print {$file} $diff;
390     close $file;
391   }
392 }
393
394 method _read_sql_file($file) {
395   return unless $file;
396
397   open my $fh, '<', $file;
398   my @data = split /;\n/, join '', <$fh>;
399   close $fh;
400
401   @data = grep {
402     $_ && # remove blank lines
403     !/^(BEGIN|BEGIN TRANSACTION|COMMIT)/ # strip txn's
404   } map {
405     s/^\s+//; s/\s+$//; # trim whitespace
406     join '', grep { !/^--/ } split /\n/ # remove comments
407   } @data;
408
409   return \@data;
410 }
411
412 sub downgrade_single_step {
413   my $self = shift;
414   my $version_set = shift @_;
415
416   my $sql = $self->_run_sql_and_perl($self->_ddl_schema_down_consume_filenames(
417     $self->storage->sqlt_type,
418     $version_set,
419   ));
420
421   return ['', $sql];
422 }
423
424 sub upgrade_single_step {
425   my $self = shift;
426   my $version_set = shift @_;
427
428   my $sql = $self->_run_sql_and_perl($self->_ddl_schema_up_consume_filenames(
429     $self->storage->sqlt_type,
430     $version_set,
431   ));
432   return ['', $sql];
433 }
434
435 __PACKAGE__->meta->make_immutable;
436
437 1;
438
439 __END__
440
441 vim: ts=2 sw=2 expandtab