disallow undef in versions
[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 # ABSTRACT: Manage your SQL and Perl migrations in nicely laid out directories
5
6 use autodie;
7 use Carp qw( carp croak );
8
9 use Method::Signatures::Simple;
10 use Try::Tiny;
11
12 use SQL::Translator;
13 require SQL::Translator::Diff;
14
15 require DBIx::Class::Storage;   # loaded for type constraint
16 use DBIx::Class::DeploymentHandler::Types;
17
18 use File::Path 'mkpath';
19 use File::Spec::Functions;
20
21 with 'DBIx::Class::DeploymentHandler::HandlesDeploy';
22
23 has schema => (
24   isa      => 'DBIx::Class::Schema',
25   is       => 'ro',
26   required => 1,
27 );
28
29 has storage => (
30   isa        => 'DBIx::Class::Storage',
31   is         => 'ro',
32   lazy_build => 1,
33 );
34
35 method _build_storage {
36   my $s = $self->schema->storage;
37   $s->_determine_driver;
38   $s
39 }
40
41 has sql_translator_args => (
42   isa => 'HashRef',
43   is  => 'ro',
44   default => sub { {} },
45 );
46 has upgrade_directory => (
47   isa      => 'Str',
48   is       => 'ro',
49   required => 1,
50   default  => 'sql',
51 );
52
53 has databases => (
54   coerce  => 1,
55   isa     => 'DBIx::Class::DeploymentHandler::Databases',
56   is      => 'ro',
57   default => sub { [qw( MySQL SQLite PostgreSQL )] },
58 );
59
60 has txn_wrap => (
61   is => 'ro',
62   isa => 'Bool',
63   default => 1,
64 );
65
66 has schema_version => (
67   is => 'ro',
68   isa => 'Str',
69   lazy_build => 1,
70 );
71
72 method _build_schema_version { $self->schema->schema_version }
73
74 method __ddl_consume_with_prefix($type, $versions, $prefix) {
75   my $base_dir = $self->upgrade_directory;
76
77   my $main    = catfile( $base_dir, $type      );
78   my $generic = catfile( $base_dir, '_generic' );
79   my $common  =
80     catfile( $base_dir, '_common', $prefix, join q(-), @{$versions} );
81
82   my $dir;
83   if (-d $main) {
84     $dir = catfile($main, $prefix, join q(-), @{$versions})
85   } elsif (-d $generic) {
86     $dir = catfile($generic, $prefix, join q(-), @{$versions});
87   } else {
88     croak "neither $main or $generic exist; please write/generate some SQL";
89   }
90
91   opendir my($dh), $dir;
92   my %files = map { $_ => "$dir/$_" } grep { /\.(?:sql|pl)$/ && -f "$dir/$_" } readdir $dh;
93   closedir $dh;
94
95   if (-d $common) {
96     opendir my($dh), $common;
97     for my $filename (grep { /\.(?:sql|pl)$/ && -f catfile($common,$_) } readdir $dh) {
98       unless ($files{$filename}) {
99         $files{$filename} = catfile($common,$filename);
100       }
101     }
102     closedir $dh;
103   }
104
105   return [@files{sort keys %files}]
106 }
107
108 method _ddl_preinstall_consume_filenames($type, $version) {
109   $self->__ddl_consume_with_prefix($type, [ $version ], 'preinstall')
110 }
111
112 method _ddl_schema_consume_filenames($type, $version) {
113   $self->__ddl_consume_with_prefix($type, [ $version ], 'schema')
114 }
115
116 method _ddl_schema_produce_filename($type, $version) {
117   my $dirname = catfile( $self->upgrade_directory, $type, 'schema', $version );
118   mkpath($dirname) unless -d $dirname;
119
120   return catfile( $dirname, '001-auto.sql' );
121 }
122
123 method _ddl_schema_up_consume_filenames($type, $versions) {
124   $self->__ddl_consume_with_prefix($type, $versions, 'up')
125 }
126
127 method _ddl_schema_down_consume_filenames($type, $versions) {
128   $self->__ddl_consume_with_prefix($type, $versions, 'down')
129 }
130
131 method _ddl_schema_up_produce_filename($type, $versions) {
132   my $dir = $self->upgrade_directory;
133
134   my $dirname = catfile( $dir, $type, 'up', join q(-), @{$versions});
135   mkpath($dirname) unless -d $dirname;
136
137   return catfile( $dirname, '001-auto.sql'
138   );
139 }
140
141 method _ddl_schema_down_produce_filename($type, $versions, $dir) {
142   my $dirname = catfile( $dir, $type, 'down', join q(-), @{$versions} );
143   mkpath($dirname) unless -d $dirname;
144
145   return catfile( $dirname, '001-auto.sql');
146 }
147
148 method _run_sql_and_perl($filenames) {
149   my @files = @{$filenames};
150   my $storage = $self->storage;
151
152
153   my $guard = $self->schema->txn_scope_guard if $self->txn_wrap;
154
155   my $sql;
156   for my $filename (@files) {
157     if ($filename =~ /\.sql$/) {
158       my @sql = @{$self->_read_sql_file($filename)};
159       $sql .= join "\n", @sql;
160
161       foreach my $line (@sql) {
162         $storage->_query_start($line);
163         try {
164           # do a dbh_do cycle here, as we need some error checking in
165           # place (even though we will ignore errors)
166           $storage->dbh_do (sub { $_[1]->do($line) });
167         }
168         catch {
169           carp "$_ (running '${line}')"
170         }
171         $storage->_query_end($line);
172       }
173     } elsif ( $filename =~ /^(.+)\.pl$/ ) {
174       my $filedata = do { local( @ARGV, $/ ) = $filename; <> };
175
176       no warnings 'redefine';
177       my $fn = eval "$filedata";
178       use warnings;
179
180       if ($@) {
181         carp "$filename failed to compile: $@";
182       } elsif (ref $fn eq 'CODE') {
183         $fn->($self->schema)
184       } else {
185         carp "$filename should define an anonymouse sub that takes a schema but it didn't!";
186       }
187     } else {
188       croak "A file ($filename) got to deploy that wasn't sql or perl!";
189     }
190   }
191
192   $guard->commit if $self->txn_wrap;
193
194   return $sql;
195 }
196
197 sub deploy {
198   my $self = shift;
199   my $version = (shift @_ || {})->{version} || $self->schema_version;
200
201   return $self->_run_sql_and_perl($self->_ddl_schema_consume_filenames(
202     $self->storage->sqlt_type,
203     $version,
204   ));
205 }
206
207 sub preinstall {
208   my $self         = shift;
209   my $args         = shift;
210   my $version      = $args->{version}      || $self->schema_version;
211   my $storage_type = $args->{storage_type} || $self->storage->sqlt_type;
212
213   my @files = @{$self->_ddl_preinstall_consume_filenames(
214     $storage_type,
215     $version,
216   )};
217
218   for my $filename (@files) {
219     # We ignore sql for now (till I figure out what to do with it)
220     if ( $filename =~ /^(.+)\.pl$/ ) {
221       my $filedata = do { local( @ARGV, $/ ) = $filename; <> };
222
223       no warnings 'redefine';
224       my $fn = eval "$filedata";
225       use warnings;
226
227       if ($@) {
228         carp "$filename failed to compile: $@";
229       } elsif (ref $fn eq 'CODE') {
230         $fn->()
231       } else {
232         carp "$filename should define an anonymous sub but it didn't!";
233       }
234     } else {
235       croak "A file ($filename) got to preinstall_scripts that wasn't sql or perl!";
236     }
237   }
238 }
239
240 sub _prepare_install {
241   my $self      = shift;
242   my $sqltargs  = { %{$self->sql_translator_args}, %{shift @_} };
243   my $to_file   = shift;
244   my $schema    = $self->schema;
245   my $databases = $self->databases;
246   my $dir       = $self->upgrade_directory;
247   my $version   = $self->schema_version;
248
249   my $sqlt = SQL::Translator->new({
250     add_drop_table          => 1,
251     ignore_constraint_names => 1,
252     ignore_index_names      => 1,
253     parser                  => 'SQL::Translator::Parser::DBIx::Class',
254     %{$sqltargs}
255   });
256
257   my $sqlt_schema = $sqlt->translate( data => $schema )
258     or croak($sqlt->error);
259
260   foreach my $db (@$databases) {
261     $sqlt->reset;
262     $sqlt->{schema} = $sqlt_schema;
263     $sqlt->producer($db);
264
265     my $filename = $self->$to_file($db, $version, $dir);
266     if (-e $filename ) {
267       carp "Overwriting existing DDL file - $filename";
268       unlink $filename;
269     }
270
271     my $output = $sqlt->translate;
272     if(!$output) {
273       carp("Failed to translate to $db, skipping. (" . $sqlt->error . ")");
274       next;
275     }
276     open my $file, q(>), $filename;
277     print {$file} $output;
278     close $file;
279   }
280 }
281
282 sub _resultsource_install_filename {
283   my ($self, $source_name) = @_;
284   return sub {
285     my ($self, $type, $version) = @_;
286     my $dirname = catfile( $self->upgrade_directory, $type, 'schema', $version );
287     mkpath($dirname) unless -d $dirname;
288
289     return catfile( $dirname, "001-auto-$source_name.sql" );
290   }
291 }
292
293 sub install_resultsource {
294   my ($self, $args) = @_;
295   my $source          = $args->{result_source};
296   my $version         = $args->{version};
297   my $rs_install_file =
298     $self->_resultsource_install_filename($source->source_name);
299
300   my $files = [
301      $self->$rs_install_file(
302       $self->storage->sqlt_type,
303       $version,
304     )
305   ];
306   $self->_run_sql_and_perl($files);
307 }
308
309 sub prepare_resultsource_install {
310   my $self = shift;
311   my $source = (shift @_)->{result_source};
312
313   my $filename = $self->_resultsource_install_filename($source->source_name);
314   $self->_prepare_install({
315       parser_args => { sources => [$source->source_name], }
316     }, $filename);
317 }
318
319 sub prepare_deploy {
320   my $self = shift;
321   $self->_prepare_install({}, '_ddl_schema_produce_filename');
322 }
323
324 sub prepare_upgrade {
325   my ($self, $args) = @_;
326   $self->_prepare_changegrade(
327     $args->{from_version}, $args->{to_version}, $args->{version_set}, 'up'
328   );
329 }
330
331 sub prepare_downgrade {
332   my ($self, $args) = @_;
333   $self->_prepare_changegrade(
334     $args->{from_version}, $args->{to_version}, $args->{version_set}, 'down'
335   );
336 }
337
338 method _prepare_changegrade($from_version, $to_version, $version_set, $direction) {
339   my $schema    = $self->schema;
340   my $databases = $self->databases;
341   my $dir       = $self->upgrade_directory;
342   my $sqltargs  = $self->sql_translator_args;
343
344   my $schema_version = $self->schema_version;
345
346   $sqltargs = {
347     add_drop_table => 1,
348     ignore_constraint_names => 1,
349     ignore_index_names => 1,
350     %{$sqltargs}
351   };
352
353   my $sqlt = SQL::Translator->new( $sqltargs );
354
355   $sqlt->parser('SQL::Translator::Parser::DBIx::Class');
356   my $sqlt_schema = $sqlt->translate( data => $schema )
357     or croak($sqlt->error);
358
359   foreach my $db (@$databases) {
360     $sqlt->reset;
361     $sqlt->{schema} = $sqlt_schema;
362     $sqlt->producer($db);
363
364     my $prefilename = $self->_ddl_schema_produce_filename($db, $from_version, $dir);
365     unless(-e $prefilename) {
366       carp("No previous schema file found ($prefilename)");
367       next;
368     }
369     my $diff_file_method = "_ddl_schema_${direction}_produce_filename";
370     my $diff_file = $self->$diff_file_method($db, $version_set, $dir );
371     if(-e $diff_file) {
372       carp("Overwriting existing $direction-diff file - $diff_file");
373       unlink $diff_file;
374     }
375
376     my $source_schema;
377     {
378       my $t = SQL::Translator->new({
379          %{$sqltargs},
380          debug => 0,
381          trace => 0,
382       });
383
384       $t->parser( $db ) # could this really throw an exception?
385         or croak($t->error);
386
387       my $out = $t->translate( $prefilename )
388         or croak($t->error);
389
390       $source_schema = $t->schema;
391
392       $source_schema->name( $prefilename )
393         unless  $source_schema->name;
394     }
395
396     # The "new" style of producers have sane normalization and can support
397     # diffing a SQL file against a DBIC->SQLT schema. Old style ones don't
398     # And we have to diff parsed SQL against parsed SQL.
399     my $dest_schema = $sqlt_schema;
400
401     unless ( "SQL::Translator::Producer::$db"->can('preprocess_schema') ) {
402       my $t = SQL::Translator->new({
403          %{$sqltargs},
404          debug => 0,
405          trace => 0,
406       });
407
408       $t->parser( $db ) # could this really throw an exception?
409         or croak($t->error);
410
411       my $filename = $self->_ddl_schema_produce_filename($db, $to_version, $dir);
412       my $out = $t->translate( $filename )
413         or croak($t->error);
414
415       $dest_schema = $t->schema;
416
417       $dest_schema->name( $filename )
418         unless $dest_schema->name;
419     }
420
421     my $diff = SQL::Translator::Diff::schema_diff(
422        $source_schema, $db,
423        $dest_schema,   $db,
424        $sqltargs
425     );
426     open my $file, q(>), $diff_file;
427     print {$file} $diff;
428     close $file;
429   }
430 }
431
432 method _read_sql_file($file) {
433   return unless $file;
434
435   open my $fh, '<', $file;
436   my @data = split /;\n/, join '', <$fh>;
437   close $fh;
438
439   @data = grep {
440     $_ && # remove blank lines
441     !/^(BEGIN|BEGIN TRANSACTION|COMMIT)/ # strip txn's
442   } map {
443     s/^\s+//; s/\s+$//; # trim whitespace
444     join '', grep { !/^--/ } split /\n/ # remove comments
445   } @data;
446
447   return \@data;
448 }
449
450 sub downgrade_single_step {
451   my $self = shift;
452   my $version_set = (shift @_)->{version_set};
453
454   my $sql = $self->_run_sql_and_perl($self->_ddl_schema_down_consume_filenames(
455     $self->storage->sqlt_type,
456     $version_set,
457   ));
458
459   return ['', $sql];
460 }
461
462 sub upgrade_single_step {
463   my $self = shift;
464   my $version_set = (shift @_)->{version_set};
465
466   my $sql = $self->_run_sql_and_perl($self->_ddl_schema_up_consume_filenames(
467     $self->storage->sqlt_type,
468     $version_set,
469   ));
470   return ['', $sql];
471 }
472
473 __PACKAGE__->meta->make_immutable;
474
475 1;
476
477 # vim: ts=2 sw=2 expandtab
478
479 __END__
480
481 =head1 DESCRIPTION
482
483 This class is the meat of L<DBIx::Class::DeploymentHandler>.  It takes care of
484 generating sql files representing schemata as well as sql files to move from
485 one version of a schema to the rest.  One of the hallmark features of this
486 class is that it allows for multiple sql files for deploy and upgrade, allowing
487 developers to fine tune deployment.  In addition it also allows for perl files
488 to be run at any stage of the process.
489
490 For basic usage see L<DBIx::Class::DeploymentHandler::HandlesDeploy>.  What's
491 documented here is extra fun stuff or private methods.
492
493 =head1 DIRECTORY LAYOUT
494
495 Arguably this is the best feature of L<DBIx::Class::DeploymentHandler>.  It's
496 heavily based upon L<DBIx::Migration::Directories>, but has some extensions and
497 modifications, so even if you are familiar with it, please read this.  I feel
498 like the best way to describe the layout is with the following example:
499
500  $sql_migration_dir
501  |- SQLite
502  |  |- down
503  |  |  `- 2-1
504  |  |     `- 001-auto.sql
505  |  |- schema
506  |  |  `- 1
507  |  |     `- 001-auto.sql
508  |  `- up
509  |     |- 1-2
510  |     |  `- 001-auto.sql
511  |     `- 2-3
512  |        `- 001-auto.sql
513  |- _common
514  |  |- down
515  |  |  `- 2-1
516  |  |     `- 002-remove-customers.pl
517  |  `- up
518  |     `- 1-2
519  |        `- 002-generate-customers.pl
520  |- _generic
521  |  |- down
522  |  |  `- 2-1
523  |  |     `- 001-auto.sql
524  |  |- schema
525  |  |  `- 1
526  |  |     `- 001-auto.sql
527  |  `- up
528  |     `- 1-2
529  |        |- 001-auto.sql
530  |        `- 002-create-stored-procedures.sql
531  `- MySQL
532     |- down
533     |  `- 2-1
534     |     `- 001-auto.sql
535     |- preinstall
536     |  `- 1
537     |     |- 001-create_database.pl
538     |     `- 002-create_users_and_permissions.pl
539     |- schema
540     |  `- 1
541     |     `- 001-auto.sql
542     `- up
543        `- 1-2
544           `- 001-auto.sql
545
546 So basically, the code
547
548  $dm->deploy(1)
549
550 on an C<SQLite> database that would simply run
551 C<$sql_migration_dir/SQLite/schema/1/001-auto.sql>.  Next,
552
553  $dm->upgrade_single_step([1,2])
554
555 would run C<$sql_migration_dir/SQLite/up/1-2/001-auto.sql> followed by
556 C<$sql_migration_dir/_common/up/1-2/002-generate-customers.pl>.
557
558 Now, a C<.pl> file doesn't have to be in the C<_common> directory, but most of
559 the time it probably should be, since perl scripts will mostly be database
560 independent.
561
562 C<_generic> exists for when you for some reason are sure that your SQL is
563 generic enough to run on all databases.  Good luck with that one.
564
565 Note that unlike most steps in the process, C<preinstall> will not run SQL, as
566 there may not even be an database at preinstall time.  It will run perl scripts
567 just like the other steps in the process, but nothing is passed to them.
568 Until people have used this more it will remain freeform, but a recommended use
569 of preinstall is to have it prompt for username and password, and then call the
570 appropriate C<< CREATE DATABASE >> commands etc.
571
572 =head1 PERL SCRIPTS
573
574 A perl script for this tool is very simple.  It merely needs to contain an
575 anonymous sub that takes a L<DBIx::Class::Schema> as it's only argument.
576 A very basic perl script might look like:
577
578  #!perl
579
580  use strict;
581  use warnings;
582
583  sub {
584    my $schema = shift;
585
586    $schema->resultset('Users')->create({
587      name => 'root',
588      password => 'root',
589    })
590  }
591
592 =attr schema
593
594 The L<DBIx::Class::Schema> (B<required>) that is used to talk to the database
595 and generate the DDL.
596
597 =attr storage
598
599 The L<DBIx::Class::Storage> that is I<actually> used to talk to the database
600 and generate the DDL.  This is automatically created with L</_build_storage>.
601
602 =attr sql_translator_args
603
604 The arguments that get passed to L<SQL::Translator> when it's used.
605
606 =attr upgrade_directory
607
608 The directory (default C<'sql'>) that upgrades are stored in
609
610 =attr databases
611
612 The types of databases (default C<< [qw( MySQL SQLite PostgreSQL )] >>) to
613 generate files for
614
615 =attr txn_wrap
616
617 Set to true (which is the default) to wrap all upgrades and deploys in a single
618 transaction.
619
620 =attr schema_version
621
622 The version the schema on your harddrive is at.  Defaults to
623 C<< $self->schema->schema_version >>.
624
625 =method __ddl_consume_with_prefix
626
627  $dm->__ddl_consume_with_prefix( 'SQLite', [qw( 1.00 1.01 )], 'up' )
628
629 This is the meat of the multi-file upgrade/deploy stuff.  It returns a list of
630 files in the order that they should be run for a generic "type" of upgrade.
631 You should not be calling this in user code.
632
633 =method _ddl_schema_consume_filenames
634
635  $dm->__ddl_schema_consume_filenames( 'SQLite', [qw( 1.00 )] )
636
637 Just a curried L</__ddl_consume_with_prefix>.  Get's a list of files for an
638 initial deploy.
639
640 =method _ddl_schema_produce_filename
641
642  $dm->__ddl_schema_produce_filename( 'SQLite', [qw( 1.00 )] )
643
644 Returns a single file in which an initial schema will be stored.
645
646 =method _ddl_schema_up_consume_filenames
647
648  $dm->_ddl_schema_up_consume_filenames( 'SQLite', [qw( 1.00 )] )
649
650 Just a curried L</__ddl_consume_with_prefix>.  Get's a list of files for an
651 upgrade.
652
653 =method _ddl_schema_down_consume_filenames
654
655  $dm->_ddl_schema_down_consume_filenames( 'SQLite', [qw( 1.00 )] )
656
657 Just a curried L</__ddl_consume_with_prefix>.  Get's a list of files for a
658 downgrade.
659
660 =method _ddl_schema_up_produce_filenames
661
662  $dm->_ddl_schema_up_produce_filename( 'SQLite', [qw( 1.00 1.01 )] )
663
664 Returns a single file in which the sql to upgrade from one schema to another
665 will be stored.
666
667 =method _ddl_schema_down_produce_filename
668
669  $dm->_ddl_schema_down_produce_filename( 'SQLite', [qw( 1.00 1.01 )] )
670
671 Returns a single file in which the sql to downgrade from one schema to another
672 will be stored.
673
674 =method _resultsource_install_filename
675
676  my $filename_fn = $dm->_resultsource_install_filename('User');
677  $dm->$filename_fn('SQLite', '1.00')
678
679 Returns a function which in turn returns a single filename used to install a
680 single resultsource.  Weird interface is convenient for me.  Deal with it.
681
682 =method _run_sql_and_perl
683
684  $dm->_run_sql_and_perl([qw( list of filenames )])
685
686 Simply put, this runs the list of files passed to it.  If the file ends in
687 C<.sql> it runs it as sql and if it ends in C<.pl> it runs it as a perl file.
688
689 Depending on L</txn_wrap> all of the files run will be wrapped in a single
690 transaction.
691
692 =method _prepare_install
693
694  $dm->_prepare_install({ add_drop_table => 0 }, sub { 'file_to_create' })
695
696 Generates the sql file for installing the database.  First arg is simply
697 L<SQL::Translator> args and the second is a coderef that returns the filename
698 to store the sql in.
699
700 =method _prepare_changegrade
701
702  $dm->_prepare_changegrade('1.00', '1.01', [qw( 1.00 1.01)], 'up')
703
704 Generates the sql file for migrating from one schema version to another.  First
705 arg is the version to start from, second is the version to go to, third is the
706 L<version set|DBIx::Class::DeploymentHandler/VERSION SET>, and last is the
707 direction of the changegrade, be it 'up' or 'down'.
708
709 =method _read_sql_file
710
711  $dm->_read_sql_file('foo.sql')
712
713 Reads a sql file and returns lines in an C<ArrayRef>.  Strips out comments,
714 transactions, and blank lines.
715