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