File Coverage

blib/lib/SQL/Translator/Diff.pm
Criterion Covered Total %
statement 196 212 92.4
branch 69 84 82.1
condition 20 25 80.0
subroutine 15 18 83.3
pod 0 10 0.0
total 300 349 85.9


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