undocument private methods
[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 script_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->script_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->script_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->script_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->script_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->script_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->script_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 script_directory
607
608 The directory (default C<'sql'>) that scripts 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 =begin comment
626
627 =head2 __ddl_consume_with_prefix
628
629  $dm->__ddl_consume_with_prefix( 'SQLite', [qw( 1.00 1.01 )], 'up' )
630
631 This is the meat of the multi-file upgrade/deploy stuff.  It returns a list of
632 files in the order that they should be run for a generic "type" of upgrade.
633 You should not be calling this in user code.
634
635 =head2 _ddl_schema_consume_filenames
636
637  $dm->__ddl_schema_consume_filenames( 'SQLite', [qw( 1.00 )] )
638
639 Just a curried L</__ddl_consume_with_prefix>.  Get's a list of files for an
640 initial deploy.
641
642 =head2 _ddl_schema_produce_filename
643
644  $dm->__ddl_schema_produce_filename( 'SQLite', [qw( 1.00 )] )
645
646 Returns a single file in which an initial schema will be stored.
647
648 =head2 _ddl_schema_up_consume_filenames
649
650  $dm->_ddl_schema_up_consume_filenames( 'SQLite', [qw( 1.00 )] )
651
652 Just a curried L</__ddl_consume_with_prefix>.  Get's a list of files for an
653 upgrade.
654
655 =head2 _ddl_schema_down_consume_filenames
656
657  $dm->_ddl_schema_down_consume_filenames( 'SQLite', [qw( 1.00 )] )
658
659 Just a curried L</__ddl_consume_with_prefix>.  Get's a list of files for a
660 downgrade.
661
662 =head2 _ddl_schema_up_produce_filenames
663
664  $dm->_ddl_schema_up_produce_filename( 'SQLite', [qw( 1.00 1.01 )] )
665
666 Returns a single file in which the sql to upgrade from one schema to another
667 will be stored.
668
669 =head2 _ddl_schema_down_produce_filename
670
671  $dm->_ddl_schema_down_produce_filename( 'SQLite', [qw( 1.00 1.01 )] )
672
673 Returns a single file in which the sql to downgrade from one schema to another
674 will be stored.
675
676 =head2 _resultsource_install_filename
677
678  my $filename_fn = $dm->_resultsource_install_filename('User');
679  $dm->$filename_fn('SQLite', '1.00')
680
681 Returns a function which in turn returns a single filename used to install a
682 single resultsource.  Weird interface is convenient for me.  Deal with it.
683
684 =head2 _run_sql_and_perl
685
686  $dm->_run_sql_and_perl([qw( list of filenames )])
687
688 Simply put, this runs the list of files passed to it.  If the file ends in
689 C<.sql> it runs it as sql and if it ends in C<.pl> it runs it as a perl file.
690
691 Depending on L</txn_wrap> all of the files run will be wrapped in a single
692 transaction.
693
694 =head2 _prepare_install
695
696  $dm->_prepare_install({ add_drop_table => 0 }, sub { 'file_to_create' })
697
698 Generates the sql file for installing the database.  First arg is simply
699 L<SQL::Translator> args and the second is a coderef that returns the filename
700 to store the sql in.
701
702 =head2 _prepare_changegrade
703
704  $dm->_prepare_changegrade('1.00', '1.01', [qw( 1.00 1.01)], 'up')
705
706 Generates the sql file for migrating from one schema version to another.  First
707 arg is the version to start from, second is the version to go to, third is the
708 L<version set|DBIx::Class::DeploymentHandler/VERSION SET>, and last is the
709 direction of the changegrade, be it 'up' or 'down'.
710
711 =head2 _read_sql_file
712
713  $dm->_read_sql_file('foo.sql')
714
715 Reads a sql file and returns lines in an C<ArrayRef>.  Strips out comments,
716 transactions, and blank lines.
717
718 =end comment