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