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