general cleanup and regex fixes so that COMMITs actually get filtered out
[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;
2e68a8e1 3use Method::Signatures::Simple;
7f50d101 4use Try::Tiny;
d23c7c77 5use SQL::Translator;
6require SQL::Translator::Diff;
7require DBIx::Class::Storage; # loaded for type constraint
3c1b5ee8 8use autodie;
85998cd9 9use File::Path;
2e68a8e1 10
7521a845 11with 'DBIx::Class::DeploymentHandler::HandlesDeploy';
3c1b5ee8 12
2e68a8e1 13use Carp 'carp';
14
d54b8d69 15has schema => (
16 isa => 'DBIx::Class::Schema',
17 is => 'ro',
18 required => 1,
19 handles => [qw( schema_version )],
20);
21
334bced5 22has storage => (
23 isa => 'DBIx::Class::Storage',
24 is => 'ro',
25 lazy_build => 1,
26);
27
2eaf903b 28method _build_storage {
29 my $s = $self->schema->storage;
30 $s->_determine_driver;
31 $s
32}
33
334bced5 34has sqltargs => (
35 isa => 'HashRef',
36 is => 'ro',
37 default => sub { {} },
38);
39has upgrade_directory => (
40 isa => 'Str',
41 is => 'ro',
42 required => 1,
43 default => 'sql',
44);
45
334bced5 46has databases => (
47 coerce => 1,
48 isa => 'DBIx::Class::DeploymentHandler::Databases',
49 is => 'ro',
50 default => sub { [qw( MySQL SQLite PostgreSQL )] },
51);
52
334bced5 53has _filedata => (
54 isa => 'ArrayRef[Str]',
55 is => 'rw',
56);
57
76d311e7 58method __ddl_consume_with_prefix($type, $versions, $prefix) {
262166c1 59 my $base_dir = $self->upgrade_directory;
60
61 my $main = File::Spec->catfile( $base_dir, $type );
62 my $generic = File::Spec->catfile( $base_dir, '_generic' );
63 my $common = File::Spec->catfile( $base_dir, '_common', $prefix, join q(-), @{$versions} );
64
65 my $dir;
66 if (-d $main) {
67 $dir = File::Spec->catfile($main, $prefix, join q(-), @{$versions})
68 } elsif (-d $generic) {
69 $dir = File::Spec->catfile($main, $prefix, join q(-), @{$versions})
70 } else {
71 die 'PREPARE TO SQL'
72 }
73
74 opendir my($dh), $dir;
75 my %files = map { $_ => "$dir/$_" } grep { /\.sql$/ && -f "$dir/$_" } readdir($dh);
76 closedir $dh;
77
78 if (-d $common) {
79 opendir my($dh), $common;
80 for my $filename (grep { /\.sql$/ && -f "$common/$_" } readdir($dh)) {
81 unless ($files{$filename}) {
82 $files{$filename} = "$common/$_";
83 }
84 }
85 closedir $dh;
86 }
87
88 return [@files{sort keys %files}]
89}
3c1b5ee8 90
76d311e7 91method _ddl_schema_consume_filenames($type, $version) {
92 $self->__ddl_consume_with_prefix($type, [ $version ], 'schema')
3c1b5ee8 93}
94
76d311e7 95method _ddl_schema_produce_filename($type, $version) {
96 my $base_dir = $self->upgrade_directory;
85998cd9 97 my $dirname = File::Spec->catfile(
76d311e7 98 $base_dir, $type, 'schema', $version
d54b8d69 99 );
85998cd9 100 File::Path::mkpath($dirname) unless -d $dirname;
d54b8d69 101
85998cd9 102 return File::Spec->catfile(
103 $dirname, '001-auto.sql'
104 );
d54b8d69 105}
106
76d311e7 107method _ddl_schema_up_consume_filenames($type, $versions) {
108 $self->__ddl_consume_with_prefix($type, $versions, 'up')
3c1b5ee8 109}
110
76d311e7 111method _ddl_schema_down_consume_filenames($type, $versions) {
112 $self->__ddl_consume_with_prefix($type, $versions, 'down')
a41a04e5 113}
114
76d311e7 115method _ddl_schema_up_produce_filename($type, $versions) {
116 my $dir = $self->upgrade_directory;
117
a41a04e5 118 my $dirname = File::Spec->catfile(
119 $dir, $type, 'up', join( q(-), @{$versions} )
120 );
121 File::Path::mkpath($dirname) unless -d $dirname;
122
123 return File::Spec->catfile(
124 $dirname, '001-auto.sql'
125 );
126}
127
76d311e7 128method _ddl_schema_down_produce_filename($type, $versions, $dir) {
85998cd9 129 my $dirname = File::Spec->catfile(
a41a04e5 130 $dir, $type, 'down', join( q(-), @{$versions} )
24f4524b 131 );
85998cd9 132 File::Path::mkpath($dirname) unless -d $dirname;
24f4524b 133
85998cd9 134 return File::Spec->catfile(
135 $dirname, '001-auto.sql'
136 );
24f4524b 137}
138
8a7847f1 139method _deployment_statements {
2e68a8e1 140 my $dir = $self->upgrade_directory;
141 my $schema = $self->schema;
142 my $type = $self->storage->sqlt_type;
143 my $sqltargs = $self->sqltargs;
9600776d 144 my $version = $self->schema_version;
2e68a8e1 145
76d311e7 146 my @filenames = @{$self->_ddl_schema_consume_filenames($type, $version)};
3c1b5ee8 147
148 for my $filename (@filenames) {
149 if(-f $filename) {
150 my $file;
151 open $file, q(<), $filename
152 or carp "Can't open $filename ($!)";
153 my @rows = <$file>;
154 close $file;
155 return join '', @rows;
156 }
2e68a8e1 157 }
158
159 # sources needs to be a parser arg, but for simplicty allow at top level
160 # coming in
161 $sqltargs->{parser_args}{sources} = delete $sqltargs->{sources}
162 if exists $sqltargs->{sources};
163
164 my $tr = SQL::Translator->new(
165 producer => "SQL::Translator::Producer::${type}",
166 %$sqltargs,
167 parser => 'SQL::Translator::Parser::DBIx::Class',
168 data => $schema,
169 );
170
284cda86 171 my $ret = $tr->translate;
2e68a8e1 172
173 $schema->throw_exception( 'Unable to produce deployment statements: ' . $tr->error)
284cda86 174 unless defined $ret;
2e68a8e1 175
284cda86 176 return $ret;
2e68a8e1 177}
178
7521a845 179sub _deploy {
180 my $self = shift;
2e68a8e1 181 my $storage = $self->storage;
182
ff7f0e7d 183#< frew> k, also, we filter out comments and transaction stuff and blank lines
184#< frew> is that really necesary?
185#< frew> and what if I want to run my upgrade in a txn? seems like something you'd
186# always want to do really
187#< ribasushi> again - some stuff chokes
188#< frew> ok, so I see filtering out -- and \s*
189#< frew> but I think the txn filtering should be optional and default to NOT filter it
190# out
191#< ribasushi> then you have a problem
192#< frew> tell me
193#< ribasushi> someone runs a deploy in txn_do
194#< ribasushi> the inner begin will blow up
195#< frew> because it's a nested TXN?
196#< ribasushi> (you an't begin twice on most dbs)
197#< ribasushi> right
198#< ribasushi> on sqlite - for sure
199#< frew> so...read the docs and set txn_filter to true?
200#< ribasushi> more like wrap deploy in a txn
201#< frew> I like that better
202#< ribasushi> and make sure the ddl has no literal txns in them
203#< frew> sure
204#< ribasushi> this way you have stuff under control
205#< frew> so we have txn_wrap default to true
206#< frew> and if people wanna do that by hand they can
77d89422 207 my $sql = $self->_deployment_statements();
cf80d86d 208 foreach my $line ( split(/;\n/, $sql)) {
209 $line = join '', grep { !/^--/ } split /\n/, $line;
210 next if !$line || $line =~ /^BEGIN TRANSACTION|^COMMIT|^\s+$/;
2e68a8e1 211 $storage->_query_start($line);
212 try {
213 # do a dbh_do cycle here, as we need some error checking in
214 # place (even though we will ignore errors)
215 $storage->dbh_do (sub { $_[1]->do($line) });
216 }
217 catch {
218 carp "$_ (running '${line}')"
219 }
220 $storage->_query_end($line);
2e68a8e1 221 }
222}
223
7521a845 224sub prepare_install {
225 my $self = shift;
2e68a8e1 226 my $schema = $self->schema;
227 my $databases = $self->databases;
228 my $dir = $self->upgrade_directory;
229 my $sqltargs = $self->sqltargs;
d54b8d69 230 my $version = $schema->schema_version;
231
2e68a8e1 232 unless( -d $dir ) {
233 carp "Upgrade directory $dir does not exist, using ./\n";
d54b8d69 234 $dir = './';
2e68a8e1 235 }
236
2e68a8e1 237
9600776d 238 my $sqlt = SQL::Translator->new({
d54b8d69 239 add_drop_table => 1,
2e68a8e1 240 ignore_constraint_names => 1,
d54b8d69 241 ignore_index_names => 1,
242 parser => 'SQL::Translator::Parser::DBIx::Class',
2e68a8e1 243 %{$sqltargs || {}}
9600776d 244 });
2e68a8e1 245
2e68a8e1 246 my $sqlt_schema = $sqlt->translate({ data => $schema })
247 or $self->throw_exception ($sqlt->error);
248
249 foreach my $db (@$databases) {
250 $sqlt->reset;
251 $sqlt->{schema} = $sqlt_schema;
252 $sqlt->producer($db);
253
76d311e7 254 my $filename = $self->_ddl_schema_produce_filename($db, $version, $dir);
9600776d 255 if (-e $filename ) {
2e68a8e1 256 carp "Overwriting existing DDL file - $filename";
257 unlink $filename;
258 }
259
260 my $output = $sqlt->translate;
261 if(!$output) {
262 carp("Failed to translate to $db, skipping. (" . $sqlt->error . ")");
263 next;
264 }
265 my $file;
266 unless( open $file, q(>), $filename ) {
267 $self->throw_exception("Can't open $filename for writing ($!)");
268 next;
269 }
270 print {$file} $output;
271 close $file;
272 }
273}
274
a41a04e5 275sub prepare_upgrade {
9600776d 276 my ($self, $from_version, $to_version, $version_set) = @_;
277
278 $from_version ||= $self->db_version;
279 $to_version ||= $self->schema_version;
280
63e95f13 281 # for updates prepared automatically (rob's stuff)
282 # one would want to explicitly set $version_set to
283 # [$to_version]
9600776d 284 $version_set ||= [$from_version, $to_version];
76d311e7 285
286 $self->_prepare_changegrade($from_version, $to_version, $version_set, 'up');
287}
288
289sub prepare_downgrade {
290 my ($self, $from_version, $to_version, $version_set) = @_;
291
292 $from_version ||= $self->db_version;
293 $to_version ||= $self->schema_version;
294
295 # for updates prepared automatically (rob's stuff)
296 # one would want to explicitly set $version_set to
297 # [$to_version]
298 $version_set ||= [$from_version, $to_version];
299
300 $self->_prepare_changegrade($from_version, $to_version, $version_set, 'down');
301}
302
303method _prepare_changegrade($from_version, $to_version, $version_set, $direction) {
2e68a8e1 304 my $schema = $self->schema;
305 my $databases = $self->databases;
306 my $dir = $self->upgrade_directory;
307 my $sqltargs = $self->sqltargs;
308
9600776d 309 my $schema_version = $schema->schema_version;
2e68a8e1 310
311 $sqltargs = {
312 add_drop_table => 1,
313 ignore_constraint_names => 1,
314 ignore_index_names => 1,
315 %{$sqltargs}
316 };
317
318 my $sqlt = SQL::Translator->new( $sqltargs );
319
320 $sqlt->parser('SQL::Translator::Parser::DBIx::Class');
321 my $sqlt_schema = $sqlt->translate({ data => $schema })
322 or $self->throw_exception ($sqlt->error);
323
324 foreach my $db (@$databases) {
325 $sqlt->reset;
326 $sqlt->{schema} = $sqlt_schema;
327 $sqlt->producer($db);
328
76d311e7 329 my $prefilename = $self->_ddl_schema_produce_filename($db, $from_version, $dir);
2e68a8e1 330 unless(-e $prefilename) {
331 carp("No previous schema file found ($prefilename)");
332 next;
333 }
76d311e7 334 my $diff_file_method = "_ddl_schema_${direction}_produce_filename";
335 my $diff_file = $self->$diff_file_method($db, $version_set, $dir );
2e68a8e1 336 if(-e $diff_file) {
76d311e7 337 carp("Overwriting existing $direction-diff file - $diff_file");
2e68a8e1 338 unlink $diff_file;
339 }
340
341 my $source_schema;
342 {
343 my $t = SQL::Translator->new({
344 %{$sqltargs},
345 debug => 0,
346 trace => 0,
347 });
348
349 $t->parser( $db ) # could this really throw an exception?
350 or $self->throw_exception ($t->error);
351
352 my $out = $t->translate( $prefilename )
353 or $self->throw_exception ($t->error);
354
355 $source_schema = $t->schema;
356
357 $source_schema->name( $prefilename )
358 unless $source_schema->name;
359 }
360
361 # The "new" style of producers have sane normalization and can support
362 # diffing a SQL file against a DBIC->SQLT schema. Old style ones don't
363 # And we have to diff parsed SQL against parsed SQL.
364 my $dest_schema = $sqlt_schema;
365
366 unless ( "SQL::Translator::Producer::$db"->can('preprocess_schema') ) {
367 my $t = SQL::Translator->new({
368 %{$sqltargs},
369 debug => 0,
370 trace => 0,
371 });
372
373 $t->parser( $db ) # could this really throw an exception?
374 or $self->throw_exception ($t->error);
375
76d311e7 376 my $filename = $self->_ddl_schema_produce_filename($db, $to_version, $dir);
2e68a8e1 377 my $out = $t->translate( $filename )
378 or $self->throw_exception ($t->error);
379
380 $dest_schema = $t->schema;
381
382 $dest_schema->name( $filename )
383 unless $dest_schema->name;
384 }
385
386 my $diff = SQL::Translator::Diff::schema_diff(
387 $source_schema, $db,
388 $dest_schema, $db,
389 $sqltargs
390 );
391 my $file;
392 unless(open $file, q(>), $diff_file) {
393 $self->throw_exception("Can't write to $diff_file ($!)");
394 next;
395 }
396 print {$file} $diff;
397 close $file;
398 }
399}
400
334bced5 401method _read_sql_file($file) {
402 return unless $file;
403
404 open my $fh, '<', $file or carp("Can't open upgrade file, $file ($!)");
405 my @data = split /\n/, join '', <$fh>;
406 close $fh;
407
408 @data = grep {
409 $_ &&
410 !/^--/ &&
411 !/^(BEGIN|BEGIN TRANSACTION|COMMIT)/m
412 } split /;/,
413 join '', @data;
414
415 return \@data;
416}
417
76d311e7 418# these are exactly the same for now
419sub _downgrade_single_step {
420 my $self = shift;
421 my @version_set = @{ shift @_ };
422 my @upgrade_files = @{$self->_ddl_schema_up_consume_filenames(
423 $self->storage->sqlt_type,
424 \@version_set,
425 )};
426
427 for my $upgrade_file (@upgrade_files) {
428 unless (-f $upgrade_file) {
429 # croak?
430 carp "Upgrade not possible, no upgrade file found ($upgrade_file), please create one\n";
431 return;
432 }
433
434 $self->_filedata($self->_read_sql_file($upgrade_file)); # I don't like this --fREW 2010-02-22
435 $self->schema->txn_do(sub { $self->_do_upgrade });
436 }
437}
438
7521a845 439sub _upgrade_single_step {
440 my $self = shift;
24f4524b 441 my @version_set = @{ shift @_ };
76d311e7 442 my @upgrade_files = @{$self->_ddl_schema_up_consume_filenames(
334bced5 443 $self->storage->sqlt_type,
24f4524b 444 \@version_set,
3c1b5ee8 445 )};
334bced5 446
3c1b5ee8 447 for my $upgrade_file (@upgrade_files) {
448 unless (-f $upgrade_file) {
449 # croak?
450 carp "Upgrade not possible, no upgrade file found ($upgrade_file), please create one\n";
451 return;
452 }
334bced5 453
3c1b5ee8 454 $self->_filedata($self->_read_sql_file($upgrade_file)); # I don't like this --fREW 2010-02-22
455 $self->schema->txn_do(sub { $self->_do_upgrade });
456 }
334bced5 457}
458
8a7847f1 459method _do_upgrade { $self->_run_upgrade(qr/.*?/) }
334bced5 460
8a7847f1 461method _run_upgrade($stm) {
334bced5 462 return unless $self->_filedata;
463 my @statements = grep { $_ =~ $stm } @{$self->_filedata};
464
465 for (@statements) {
466 $self->storage->debugobj->query_start($_) if $self->storage->debug;
8a7847f1 467 $self->_apply_statement($_);
334bced5 468 $self->storage->debugobj->query_end($_) if $self->storage->debug;
469 }
470}
471
8a7847f1 472method _apply_statement($statement) {
334bced5 473 # croak?
474 $self->storage->dbh->do($_) or carp "SQL was: $_"
475}
476
2e68a8e1 4771;
e051bb00 478
479__END__
480
2eaf903b 481vim: ts=2 sw=2 expandtab