File Coverage

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


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