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