File Coverage

blib/lib/SQL/Translator/Diff.pm
Criterion Covered Total %
statement 195 209 93.3
branch 69 84 82.1
condition 20 25 80.0
subroutine 15 17 88.2
pod 0 9 0.0
total 299 344 86.9


line stmt bran cond sub pod time code
1             package SQL::Translator::Diff;
2              
3              
4             ## SQLT schema diffing code
5 13     13   10580 use strict;
  13         33  
  13         376  
6 13     13   75 use warnings;
  13         34  
  13         366  
7              
8 13     13   70 use Data::Dumper;
  13         25  
  13         715  
9 13     13   84 use Carp::Clan qw/^SQL::Translator/;
  13         26  
  13         110  
10 13     13   1489 use SQL::Translator::Schema::Constants;
  13         30  
  13         982  
11 13     13   89 use Sub::Quote qw(quote_sub);
  13         25  
  13         532  
12 13     13   70 use Moo;
  13         63  
  13         105  
13              
14             has ignore_index_names => (
15             is => 'rw',
16             );
17             has ignore_constraint_names => (
18             is => 'rw',
19             );
20             has ignore_view_sql => (
21             is => 'rw',
22             );
23             has ignore_proc_sql => (
24             is => 'rw',
25             );
26             has output_db => (
27             is => 'rw',
28             );
29             has source_schema => (
30             is => 'rw',
31             );
32             has target_schema => (
33             is => 'rw',
34             );
35             has case_insensitive => (
36             is => 'rw',
37             );
38             has no_batch_alters => (
39             is => 'rw',
40             );
41             has ignore_missing_methods => (
42             is => 'rw',
43             );
44             has producer_args => (
45             is => 'rw',
46             lazy => 1,
47             default => quote_sub '{}',
48             );
49             has tables_to_drop => (
50             is => 'rw',
51             lazy => 1,
52             default => quote_sub '[]',
53             );
54             has tables_to_create => (
55             is => 'rw',
56             lazy => 1,
57             default => quote_sub '[]',
58             );
59             has table_diff_hash => (
60             is => 'rw',
61             lazy => 1,
62             default => quote_sub '{}',
63             );
64              
65             my @diff_arrays = qw/
66             tables_to_drop
67             tables_to_create
68             /;
69              
70             my @diff_hash_keys = qw/
71             constraints_to_create
72             constraints_to_drop
73             indexes_to_create
74             indexes_to_drop
75             fields_to_create
76             fields_to_alter
77             fields_to_rename
78             fields_to_drop
79             table_options
80             table_renamed_from
81             /;
82              
83             sub schema_diff {
84             # use Data::Dumper;
85             ## we are getting instructions on how to turn the source into the target
86             ## source == original, target == new (hmm, if I need to comment this, should I rename the vars again ??)
87             ## _schema isa SQL::Translator::Schema
88             ## _db is the name of the producer/db it came out of/into
89             ## results are formatted to the source preferences
90              
91 16     16 0 9198 my ($source_schema, $source_db, $target_schema, $output_db, $options) = @_;
92 16   100     78 $options ||= {};
93              
94 16         431 my $obj = SQL::Translator::Diff->new( {
95             %$options,
96             source_schema => $source_schema,
97             target_schema => $target_schema,
98             output_db => $output_db
99             } );
100              
101 16         93 $obj->compute_differences->produce_diff_sql;
102             }
103              
104             sub BUILD {
105 22     22 0 33081 my ($self, $args) = @_;
106 22 50       109 if ($args->{producer_options}) {
107 0         0 carp 'producer_options is deprecated. Please use producer_args';
108             $self->producer_args({
109 0         0 %{$args->{producer_options}},
110 0         0 %{$self->producer_args}
  0         0  
111             });
112             }
113              
114 22 100       183 if (! $self->output_db) {
115             $self->output_db($args->{source_db})
116 3         26 }
117             }
118              
119             sub compute_differences {
120 22     22 0 73 my ($self) = @_;
121              
122 22         76 my $target_schema = $self->target_schema;
123 22         68 my $source_schema = $self->source_schema;
124              
125 22         51 my $producer_class = "SQL::Translator::Producer::@{[$self->output_db]}";
  22         143  
126 22         1664 eval "require $producer_class";
127 22 50       153 die $@ if $@;
128              
129 22 100       302 if (my $preprocess = $producer_class->can('preprocess_schema')) {
130 14         64 $preprocess->($source_schema);
131 14         56 $preprocess->($target_schema);
132             }
133              
134 22         137 my %src_tables_checked = ();
135 22         115 my @tar_tables = sort { $a->name cmp $b->name } $target_schema->get_tables;
  69         2197  
136             ## do original/source tables exist in target?
137 22         406 for my $tar_table ( @tar_tables ) {
138 66         1469 my $tar_table_name = $tar_table->name;
139              
140 66         1260 my $src_table;
141              
142             $self->table_diff_hash->{$tar_table_name} = {
143 66         156 map {$_ => [] } @diff_hash_keys
  660         2542  
144             };
145              
146 66 100       1777 if (my $old_name = $tar_table->extra('renamed_from')) {
147 10         65 $src_table = $source_schema->get_table( $old_name, $self->case_insensitive );
148 10 50       41 if ($src_table) {
149 10         392 $self->table_diff_hash->{$tar_table_name}{table_renamed_from} = [ [$src_table, $tar_table] ];
150             } else {
151 0         0 delete $tar_table->extra->{renamed_from};
152 0         0 carp qq#Renamed table can't find old table "$old_name" for renamed table\n#;
153             }
154             } else {
155 56         304 $src_table = $source_schema->get_table( $tar_table_name, $self->case_insensitive );
156             }
157              
158 66 100       368 unless ( $src_table ) {
159             ## table is new
160             ## add table(s) later.
161 11         23 push @{$self->tables_to_create}, $tar_table;
  11         207  
162 11         147 next;
163             }
164              
165 55         2028 my $src_table_name = $src_table->name;
166 55 50       1099 $src_table_name = lc $src_table_name if $self->case_insensitive;
167 55         175 $src_tables_checked{$src_table_name} = 1;
168              
169              
170 55         224 $self->diff_table_options($src_table, $tar_table);
171              
172             ## Compare fields, their types, defaults, sizes etc etc
173 55         455 $self->diff_table_fields($src_table, $tar_table);
174              
175 55         1042 $self->diff_table_indexes($src_table, $tar_table);
176 55         367 $self->diff_table_constraints($src_table, $tar_table);
177              
178             } # end of target_schema->get_tables loop
179              
180 22         469 for my $src_table ( $source_schema->get_tables ) {
181 68         1433 my $src_table_name = $src_table->name;
182              
183 68 50       1402 $src_table_name = lc $src_table_name if $self->case_insensitive;
184              
185 13         274 push @{ $self->tables_to_drop}, $src_table
186 68 100       266 unless $src_tables_checked{$src_table_name};
187             }
188              
189 22         266 return $self;
190             }
191              
192             sub produce_diff_sql {
193 19     19 0 61 my ($self) = @_;
194              
195 19         94 my $target_schema = $self->target_schema;
196 19         70 my $source_schema = $self->source_schema;
197 19         100 my $tar_name = $target_schema->name;
198 19         75 my $src_name = $source_schema->name;
199              
200 19         53 my $producer_class = "SQL::Translator::Producer::@{[$self->output_db]}";
  19         134  
201 19         1722 eval "require $producer_class";
202 19 50       120 die $@ if $@;
203              
204             # Map of name we store under => producer method name
205 19         213 my %func_map = (
206             constraints_to_create => 'alter_create_constraint',
207             constraints_to_drop => 'alter_drop_constraint',
208             indexes_to_create => 'alter_create_index',
209             indexes_to_drop => 'alter_drop_index',
210             fields_to_create => 'add_field',
211             fields_to_alter => 'alter_field',
212             fields_to_rename => 'rename_field',
213             fields_to_drop => 'drop_field',
214             table_options => 'alter_table',
215             table_renamed_from => 'rename_table',
216             );
217 19         42 my @diffs;
218              
219 19 100 100     323 if (!$self->no_batch_alters &&
220             (my $batch_alter = $producer_class->can('batch_alter_table')) )
221             {
222             # Good - Producer supports batch altering of tables.
223 15         42 foreach my $table ( sort keys %{$self->table_diff_hash} ) {
  15         353  
224 44   33     345 my $tar_table = $target_schema->get_table($table)
225             || $source_schema->get_table($table);
226              
227             push @diffs, $batch_alter->($tar_table,
228             { map {
229 44         998 $func_map{$_} => $self->table_diff_hash->{$table}{$_}
  440         8397  
230             } keys %func_map
231             },
232             $self->producer_args
233             );
234             }
235             } else {
236              
237             # If we have any table renames we need to do those first;
238 4         12 my %flattened_diffs;
239 4         11 foreach my $table ( sort keys %{$self->table_diff_hash} ) {
  4         80  
240 10         223 my $table_diff = $self->table_diff_hash->{$table};
241 10         74 for (@diff_hash_keys) {
242 100   100     122 push( @{ $flattened_diffs{ $func_map{$_} } ||= [] }, @{ $table_diff->{$_} } );
  100         288  
  100         208  
243             }
244             }
245              
246             push @diffs, map( {
247 4 50       26 if (@{ $flattened_diffs{$_} || [] }) {
  40 100       61  
  40         124  
248 22         142 my $meth = $producer_class->can($_);
249              
250             $meth ? map {
251 31 100       655 map { $_ ? "$_" : () } $meth->( (ref $_ eq 'ARRAY' ? @$_ : $_), $self->producer_args );
  30 100       282  
252 22 50       67 } @{ $flattened_diffs{$_} }
  19 100       56  
253             : $self->ignore_missing_methods
254             ? "-- $producer_class cant $_"
255             : die "$producer_class cant $_";
256 18         35 } else { () }
257              
258             } qw/rename_table
259             alter_drop_constraint
260             alter_drop_index
261             drop_field
262             add_field
263             alter_field
264             rename_field
265             alter_create_index
266             alter_create_constraint
267             alter_table/),
268             }
269              
270 19 100       81 if (my @tables = @{ $self->tables_to_create } ) {
  19         428  
271             my $translator = SQL::Translator->new(
272             producer_type => $self->output_db,
273             add_drop_table => 0,
274             no_comments => 1,
275             # TODO: sort out options
276 9         156 %{ $self->producer_args }
  9         200  
277             );
278 9         399 $translator->producer_args->{no_transaction} = 1;
279 9         209 my $schema = $translator->schema;
280              
281 9         817 $schema->add_table($_) for @tables;
282              
283             unshift @diffs,
284             # Remove begin/commit here, since we wrap everything in one.
285 9         112 grep { $_ !~ /^(?:COMMIT|START(?: TRANSACTION)?|BEGIN(?: TRANSACTION)?)/ } $producer_class->can('produce')->($translator);
  19         214  
286             }
287              
288 19 100       189 if (my @tables_to_drop = @{ $self->{tables_to_drop} || []} ) {
  19 100       166  
289 11         84 my $meth = $producer_class->can('drop_table');
290              
291 11 0       55 push @diffs, $meth ? ( map { $meth->($_, $self->producer_args) } @tables_to_drop)
  11 50       227  
292             : $self->ignore_missing_methods
293             ? "-- $producer_class cant drop_table"
294             : die "$producer_class cant drop_table";
295             }
296              
297 19 100       234 if (@diffs) {
298 15         65 unshift @diffs, "BEGIN";
299 15         42 push @diffs, "\nCOMMIT";
300             } else {
301 4         19 @diffs = ("-- No differences found");
302             }
303              
304 19 50       69 if ( @diffs ) {
305 19 100       171 if ( $self->output_db !~ /^(?:MySQL|SQLite|PostgreSQL)$/ ) {
306 2         7 unshift(@diffs, "-- Output database @{[$self->output_db]} is untested/unsupported!!!");
  2         15  
307             }
308              
309             my @return =
310 19 100       105 map { $_ ? ( $_ =~ /;\s*\z/xms ? $_ : "$_;\n\n" ) : "\n" }
  189 50       674  
311             ("-- Convert schema '$src_name' to '$tar_name':", @diffs);
312              
313 19 100       347 return wantarray ? @return : join('', @return);
314             }
315 0         0 return undef;
316              
317             }
318              
319             sub diff_table_indexes {
320 55     55 0 204 my ($self, $src_table, $tar_table) = @_;
321              
322 55         107 my (%checked_indices);
323             INDEX_CREATE:
324 55         213 for my $i_tar ( $tar_table->get_indices ) {
325 16         247 for my $i_src ( $src_table->get_indices ) {
326 16 100       520 if ( $i_tar->equals($i_src, $self->case_insensitive, $self->ignore_index_names) ) {
327 9         38 $checked_indices{$i_src} = 1;
328 9         38 next INDEX_CREATE;
329             }
330             }
331 7         131 push @{$self->table_diff_hash->{$tar_table}{indexes_to_create}}, $i_tar;
  7         128  
332             }
333              
334             INDEX_DROP:
335 55         335 for my $i_src ( $src_table->get_indices ) {
336 16 100 100     267 next if !$self->ignore_index_names && $checked_indices{$i_src};
337 11         47 for my $i_tar ( $tar_table->get_indices ) {
338 11 100       309 next INDEX_DROP if $i_src->equals($i_tar, $self->case_insensitive, $self->ignore_index_names);
339             }
340 7         97 push @{$self->table_diff_hash->{$tar_table}{indexes_to_drop}}, $i_src;
  7         137  
341             }
342             }
343              
344              
345             sub diff_table_constraints {
346 55     55 0 137 my ($self, $src_table, $tar_table) = @_;
347              
348 55         96 my(%checked_constraints);
349             CONSTRAINT_CREATE:
350 55         216 for my $c_tar ( $tar_table->get_constraints ) {
351 110         1132 for my $c_src ( $src_table->get_constraints ) {
352              
353             # This is a bit of a hack - needed for renaming tables to work
354 180         2612 local $c_src->{table} = $tar_table;
355              
356 180 100       3856 if ( $c_tar->equals($c_src, $self->case_insensitive, $self->ignore_constraint_names) ) {
357 75         260 $checked_constraints{$c_src} = 1;
358 75         299 next CONSTRAINT_CREATE;
359             }
360             }
361 35         512 push @{ $self->table_diff_hash->{$tar_table}{constraints_to_create} }, $c_tar;
  35         625  
362             }
363              
364              
365             CONSTRAINT_DROP:
366 55         590 for my $c_src ( $src_table->get_constraints ) {
367              
368             # This is a bit of a hack - needed for renaming tables to work
369 99         876 local $c_src->{table} = $tar_table;
370              
371 99 100 100     753 next if !$self->ignore_constraint_names && $checked_constraints{$c_src};
372 40         142 for my $c_tar ( $tar_table->get_constraints ) {
373 86 100       2705 next CONSTRAINT_DROP if $c_src->equals($c_tar, $self->case_insensitive, $self->ignore_constraint_names);
374             }
375              
376 24         165 push @{ $self->table_diff_hash->{$tar_table}{constraints_to_drop} }, $c_src;
  24         454  
377             }
378              
379             }
380              
381             sub diff_table_fields {
382 55     55 0 140 my ($self, $src_table, $tar_table) = @_;
383              
384             # List of ones we've renamed from so we don't drop them
385 55         97 my %renamed_source_fields;
386              
387 55         192 for my $tar_table_field ( $tar_table->get_fields ) {
388 200         4408 my $f_tar_name = $tar_table_field->name;
389              
390 200 100       7516 if (my $old_name = $tar_table_field->extra->{renamed_from}) {
391 9         59 my $src_table_field = $src_table->get_field( $old_name, $self->case_insensitive );
392 9 50       88 unless ($src_table_field) {
393 0         0 carp qq#Renamed column can't find old column "@{[$src_table->name]}.$old_name" for renamed column\n#;
  0         0  
394 0         0 delete $tar_table_field->extra->{renamed_from};
395             } else {
396 9         190 push @{$self->table_diff_hash->{$tar_table}{fields_to_rename} }, [ $src_table_field, $tar_table_field ];
  9         186  
397 9         213 $renamed_source_fields{$old_name} = 1;
398 9         33 next;
399             }
400             }
401              
402 191         864 my $src_table_field = $src_table->get_field( $f_tar_name, $self->case_insensitive );
403              
404 191 100       1830 unless ( $src_table_field ) {
405 26         59 push @{$self->table_diff_hash->{$tar_table}{fields_to_create}}, $tar_table_field;
  26         430  
406 26         614 next;
407             }
408              
409             # field exists, something changed. This is a bit complex. Parsers can
410             # normalize types, but only some of them do, so compare the normalized and
411             # parsed types for each field to each other
412 165 50 100     6573 if ( !$tar_table_field->equals($src_table_field, $self->case_insensitive) &&
      66        
      33        
413             !$tar_table_field->equals($src_table_field->parsed_field, $self->case_insensitive) &&
414             !$tar_table_field->parsed_field->equals($src_table_field, $self->case_insensitive) &&
415             !$tar_table_field->parsed_field->equals($src_table_field->parsed_field, $self->case_insensitive) ) {
416              
417             # Some producers might need src field to diff against
418 48         655 push @{$self->table_diff_hash->{$tar_table}{fields_to_alter}}, [ $src_table_field, $tar_table_field ];
  48         831  
419 48         1158 next;
420             }
421             }
422              
423              
424             # Now check to see if any fields from src_table need to be dropped
425 55         283 for my $src_table_field ( $src_table->get_fields ) {
426 188         5817 my $f_src_name = $src_table_field->name;
427 188 100       3706 next if $renamed_source_fields{$f_src_name};
428              
429 179         779 my $tar_table_field = $tar_table->get_field( $f_src_name, $self->case_insensitive );
430              
431 179 100       1451 unless ( $tar_table_field ) {
432 14         33 push @{$self->table_diff_hash->{$tar_table}{fields_to_drop}}, $src_table_field;
  14         244  
433 14         387 next;
434             }
435             }
436             }
437              
438             sub diff_table_options {
439 55     55 0 150 my ($self, $src_table, $tar_table) = @_;
440              
441             my $cmp = sub {
442 0     0   0 my ($a_name, undef, $b_name, undef) = ( %$a, %$b );
443 0         0 $a_name cmp $b_name;
444 55         255 };
445             # Need to sort the options so we don't get spurious diffs.
446 55         129 my (@src_opts, @tar_opts);
447 55         1230 @src_opts = sort $cmp $src_table->options;
448 55         1057 @tar_opts = sort $cmp $tar_table->options;
449              
450              
451             # If there's a difference, just re-set all the options
452 55 100       345 push @{ $self->table_diff_hash->{$tar_table}{table_options} }, $tar_table
  9         212  
453             unless $src_table->_compare_objects( \@src_opts, \@tar_opts );
454             }
455              
456             # support producer_options as an alias for producer_args for legacy code.
457             sub producer_options {
458 0     0 0   my $self = shift;
459              
460 0           return $self->producer_args( @_ );
461             }
462              
463             1;
464              
465             __END__