File Coverage

blib/lib/MySQL/Diff.pm
Criterion Covered Total %
statement 15 259 5.7
branch 0 144 0.0
condition 0 104 0.0
subroutine 5 23 21.7
pod 5 5 100.0
total 25 535 4.6


line stmt bran cond sub pod time code
1             package MySQL::Diff;
2              
3             =head1 NAME
4              
5             MySQL::Diff - Generates a database upgrade instruction set
6              
7             =head1 SYNOPSIS
8              
9             use MySQL::Diff;
10              
11             my $md = MySQL::Diff->new( %options );
12             my $db1 = $md->register_db($ARGV[0], 1);
13             my $db2 = $md->register_db($ARGV[1], 2);
14             my $diffs = $md->diff();
15              
16             =head1 DESCRIPTION
17              
18             Generates the SQL instructions required to upgrade the first database to match
19             the second.
20              
21             =cut
22              
23 2     2   116604 use warnings;
  2         11  
  2         63  
24 2     2   9 use strict;
  2         3  
  2         66  
25              
26             our $VERSION = '0.60';
27              
28             # ------------------------------------------------------------------------------
29             # Libraries
30              
31 2     2   763 use MySQL::Diff::Database;
  2         14  
  2         62  
32 2     2   11 use MySQL::Diff::Utils qw(debug debug_level debug_file);
  2         5  
  2         85  
33              
34 2     2   1069 use Data::Dumper;
  2         11332  
  2         6015  
35              
36             # ------------------------------------------------------------------------------
37              
38             =head1 METHODS
39              
40             =head2 Constructor
41              
42             =over 4
43              
44             =item new( %options )
45              
46             Instantiate the objects, providing the command line options for database
47             access and process requirements.
48              
49             =back
50              
51             =cut
52              
53             sub new {
54 0     0 1   my $class = shift;
55 0           my %hash = @_;
56 0           my $self = {};
57 0   0       bless $self, ref $class || $class;
58              
59 0           $self->{opts} = \%hash;
60              
61 0 0         if($hash{debug}) { debug_level($hash{debug}) ; delete $hash{debug}; }
  0            
  0            
62 0 0         if($hash{debug_file}) { debug_file($hash{debug_file}) ; delete $hash{debug_file}; }
  0            
  0            
63              
64 0           debug(3,"\nconstructing new MySQL::Diff");
65              
66 0           return $self;
67             }
68              
69             =head2 Public Methods
70              
71             Fuller documentation will appear here in time :)
72              
73             =over 4
74              
75             =item * register_db($name,$inx)
76              
77             Reference the database, and setup a connection. The name can be an already
78             existing 'MySQL::Diff::Database' database object. The index can be '1' or '2',
79             and refers both to the order of the diff, and to the host, port, username and
80             password arguments that have been supplied.
81              
82             =cut
83              
84             sub register_db {
85 0     0 1   my ($self, $name, $inx) = @_;
86 0 0 0       return unless $inx == 1 || $inx == 2;
87              
88 0 0         my $db = ref $name eq 'MySQL::Diff::Database' ? $name : $self->_load_database($name,$inx);
89 0           $self->{databases}[$inx-1] = $db;
90 0           return $db;
91             }
92              
93             =item * db1()
94              
95             =item * db2()
96              
97             Return the first and second databases registered via C.
98              
99             =cut
100              
101 0     0 1   sub db1 { shift->{databases}->[0] }
102 0     0 1   sub db2 { shift->{databases}->[1] }
103              
104             =item * diff()
105              
106             Performs the diff, returning a string containing the commands needed to change
107             the schema of the first database into that of the second.
108              
109             =back
110              
111             =cut
112              
113             sub diff {
114 0     0 1   my $self = shift;
115 0           my @changes;
116 0           my %used_tables = ();
117              
118 0           debug(1, "\ncomparing databases");
119              
120 0           for my $table1 ($self->db1->tables()) {
121 0           my $name = $table1->name();
122 0           $used_tables{'-- '. $name} = 1;
123 0           debug(4, "table 1 $name = ".Dumper($table1));
124 0           debug(2,"looking at tables called '$name'");
125 0 0         if (my $table2 = $self->db2->table_by_name($name)) {
126 0           debug(3,"comparing tables called '$name'");
127 0           push @changes, $self->_diff_tables($table1, $table2);
128             } else {
129 0           debug(3,"table '$name' dropped");
130             push @changes, "DROP TABLE $name;\n\n"
131 0 0 0       unless $self->{opts}{'only-both'} || $self->{opts}{'keep-old-tables'};
132             }
133             }
134              
135 0           for my $table2 ($self->db2->tables()) {
136 0           my $name = $table2->name();
137 0           $used_tables{'-- '. $name} = 1;
138 0           debug(4, "table 2 $name = ".Dumper($table2));
139 0 0         if (! $self->db1->table_by_name($name)) {
140 0           debug(3,"table '$name' added");
141 0           debug(4,"table '$name' added '".$table2->def()."'");
142             push @changes, $table2->def() . "\n"
143 0 0         unless $self->{opts}{'only-both'};
144             }
145             }
146              
147 0           debug(4,join '', @changes);
148              
149 0           my $out = '';
150 0 0         if (@changes) {
151 0 0         if (!$self->{opts}{'list-tables'}) {
152 0           $out .= $self->_diff_banner();
153             }
154             else {
155 0           $out .= "-- TABLES LIST \n";
156 0           $out .= join "\n", keys %used_tables;
157 0           $out .= "\n-- END OF TABLES LIST \n";
158             }
159 0           $out .= join '', @changes;
160             }
161 0           return $out;
162             }
163              
164             # ------------------------------------------------------------------------------
165             # Private Methods
166              
167             sub _diff_banner {
168 0     0     my ($self) = @_;
169              
170 0           my $summary1 = $self->db1->summary();
171 0           my $summary2 = $self->db2->summary();
172              
173             my $opt_text =
174             join ', ',
175 0 0         map { $self->{opts}{$_} eq '1' ? $_ : "$_=$self->{opts}{$_}" }
176 0           keys %{$self->{opts}};
  0            
177 0 0         $opt_text = "## Options: $opt_text\n" if $opt_text;
178              
179 0           my $now = scalar localtime();
180 0           return <
181             ## mysqldiff $VERSION
182             ##
183             ## Run on $now
184             $opt_text##
185             ## --- $summary1
186             ## +++ $summary2
187              
188             EOF
189             }
190              
191             sub _diff_tables {
192 0     0     my $self = shift;
193 0           my @changes = (
194             $self->_diff_fields(@_),
195             $self->_diff_indices(@_),
196             $self->_diff_primary_key(@_),
197             $self->_diff_foreign_key(@_),
198             $self->_diff_options(@_)
199             );
200              
201 0 0         $changes[-1] =~ s/\n*$/\n/ if (@changes);
202 0           return @changes;
203             }
204              
205             sub _diff_fields {
206 0     0     my ($self, $table1, $table2) = @_;
207              
208 0           my $name1 = $table1->name();
209              
210 0           my $fields1 = $table1->fields;
211 0           my $fields2 = $table2->fields;
212              
213 0 0 0       return () unless $fields1 || $fields2;
214              
215 0           my @changes;
216            
217 0 0         if($fields1) {
218 0           for my $field (keys %$fields1) {
219 0           debug(3,"table1 had field '$field'");
220 0           my $f1 = $fields1->{$field};
221 0           my $f2 = $fields2->{$field};
222 0 0 0       if ($fields2 && $f2) {
    0          
223 0 0         if ($self->{opts}{tolerant}) {
224 0           for ($f1, $f2) {
225 0           s/ COLLATE [\w_]+//gi;
226             }
227             }
228 0 0         if ($f1 ne $f2) {
229 0 0 0       if (not $self->{opts}{tolerant} or
      0        
      0        
230             (($f1 !~ m/$f2\(\d+,\d+\)/) and
231             ($f1 ne "$f2 DEFAULT '' NOT NULL") and
232             ($f1 ne "$f2 NOT NULL") ))
233             {
234 0           debug(3,"field '$field' changed");
235              
236 0           my $change = "ALTER TABLE $name1 CHANGE COLUMN $field $field $f2;";
237 0 0         $change .= " # was $f1" unless $self->{opts}{'no-old-defs'};
238 0           $change .= "\n";
239 0           push @changes, $change;
240             }
241             }
242             } elsif (!$self->{opts}{'keep-old-columns'}) {
243 0           debug(3,"field '$field' removed");
244 0           my $change = "ALTER TABLE $name1 DROP COLUMN $field;";
245 0 0         $change .= " # was $fields1->{$field}" unless $self->{opts}{'no-old-defs'};
246 0           $change .= "\n";
247 0           push @changes, $change;
248             }
249             }
250             }
251              
252 0 0         if($fields2) {
253 0           for my $field (keys %$fields2) {
254 0 0 0       unless($fields1 && $fields1->{$field}) {
255 0           debug(3,"field '$field' added");
256 0           my $changes = "ALTER TABLE $name1 ADD COLUMN $field $fields2->{$field}";
257 0 0         if ($table2->is_auto_inc($field)) {
258 0 0         if ($table2->isa_primary($field)) {
    0          
259 0           $changes .= ' PRIMARY KEY';
260             } elsif ($table2->is_unique($field)) {
261 0           $changes .= ' UNIQUE KEY';
262             }
263             }
264 0           push @changes, "$changes;\n";
265             }
266             }
267             }
268              
269 0           return @changes;
270             }
271              
272             sub _diff_indices {
273 0     0     my ($self, $table1, $table2) = @_;
274              
275 0           my $name1 = $table1->name();
276              
277 0           my $indices1 = $table1->indices();
278 0           my $indices2 = $table2->indices();
279              
280 0 0 0       return () unless $indices1 || $indices2;
281              
282 0           my @changes;
283              
284 0 0         if($indices1) {
285 0           for my $index (keys %$indices1) {
286 0           debug(3,"table1 had index '$index'");
287 0 0         my $old_type = $table1->is_unique($index) ? 'UNIQUE' :
    0          
288             $table1->is_fulltext($index) ? 'FULLTEXT INDEX' : 'INDEX';
289              
290 0 0 0       if ($indices2 && $indices2->{$index}) {
291 0 0 0       if( ($indices1->{$index} ne $indices2->{$index}) or
      0        
      0        
      0        
292             ($table1->is_unique($index) xor $table2->is_unique($index)) or
293             ($table1->is_fulltext($index) xor $table2->is_fulltext($index)) )
294             {
295 0           debug(3,"index '$index' changed");
296 0 0         my $new_type = $table2->is_unique($index) ? 'UNIQUE' :
    0          
297             $table2->is_fulltext($index) ? 'FULLTEXT INDEX' : 'INDEX';
298              
299 0           my $changes = "ALTER TABLE $name1 DROP INDEX $index;";
300             $changes .= " # was $old_type ($indices1->{$index})"
301 0 0         unless $self->{opts}{'no-old-defs'};
302 0           $changes .= "\nALTER TABLE $name1 ADD $new_type $index ($indices2->{$index});\n";
303 0           push @changes, $changes;
304             }
305             } else {
306 0           debug(3,"index '$index' removed");
307 0   0       my $auto = _check_for_auto_col($table2, $indices1->{$index}, 1) || '';
308 0 0         my $changes = $auto ? _index_auto_col($table1, $indices1->{$index}) : '';
309 0           $changes .= "ALTER TABLE $name1 DROP INDEX $index;";
310             $changes .= " # was $old_type ($indices1->{$index})"
311 0 0         unless $self->{opts}{'no-old-defs'};
312 0           $changes .= "\n";
313 0           push @changes, $changes;
314             }
315             }
316             }
317              
318 0 0         if($indices2) {
319 0           for my $index (keys %$indices2) {
320 0 0 0       next if($indices1 && $indices1->{$index});
321             next if(
322 0 0 0       !$table2->isa_primary($index) &&
      0        
323             $table2->is_unique($index) &&
324             _key_covers_auto_col($table2, $index)
325             );
326 0           debug(3,"index '$index' added");
327 0 0         my $new_type = $table2->is_unique($index) ? 'UNIQUE' : 'INDEX';
328 0           push @changes, "ALTER TABLE $name1 ADD $new_type $index ($indices2->{$index});\n";
329             }
330             }
331              
332 0           return @changes;
333             }
334              
335             sub _diff_primary_key {
336 0     0     my ($self, $table1, $table2) = @_;
337              
338 0           my $name1 = $table1->name();
339              
340 0           my $primary1 = $table1->primary_key();
341 0           my $primary2 = $table2->primary_key();
342              
343 0 0 0       return () unless $primary1 || $primary2;
344              
345 0           my @changes;
346            
347 0 0 0       if ($primary1 && ! $primary2) {
348 0           debug(3,"primary key '$primary1' dropped");
349 0           my $changes = _index_auto_col($table2, $primary1);
350 0           $changes .= "ALTER TABLE $name1 DROP PRIMARY KEY;";
351 0 0         $changes .= " # was $primary1" unless $self->{opts}{'no-old-defs'};
352 0           return ( "$changes\n" );
353             }
354              
355 0 0 0       if (! $primary1 && $primary2) {
356 0           debug(3,"primary key '$primary2' added");
357 0 0         return () if _key_covers_auto_col($table2, $primary2);
358 0           return ("ALTER TABLE $name1 ADD PRIMARY KEY $primary2;\n");
359             }
360              
361 0 0         if ($primary1 ne $primary2) {
362 0           debug(3,"primary key changed");
363 0   0       my $auto = _check_for_auto_col($table2, $primary1) || '';
364 0 0         my $changes = $auto ? _index_auto_col($table2, $auto) : '';
365 0           $changes .= "ALTER TABLE $name1 DROP PRIMARY KEY;";
366 0 0         $changes .= " # was $primary1" unless $self->{opts}{'no-old-defs'};
367 0           $changes .= "\nALTER TABLE $name1 ADD PRIMARY KEY $primary2;\n";
368 0 0         $changes .= "ALTER TABLE $name1 DROP INDEX $auto;\n" if($auto);
369 0           push @changes, $changes;
370             }
371              
372 0           return @changes;
373             }
374              
375             sub _diff_foreign_key {
376 0     0     my ($self, $table1, $table2) = @_;
377              
378 0           my $name1 = $table1->name();
379              
380 0           my $fks1 = $table1->foreign_key();
381 0           my $fks2 = $table2->foreign_key();
382              
383 0 0 0       return () unless $fks1 || $fks2;
384              
385 0           my @changes;
386            
387 0 0         if($fks1) {
388 0           for my $fk (keys %$fks1) {
389 0           debug(1,"$name1 has fk '$fk'");
390              
391 0 0 0       if ($fks2 && $fks2->{$fk}) {
392 0 0         if($fks1->{$fk} ne $fks2->{$fk})
393             {
394 0           debug(1,"foreign key '$fk' changed");
395 0           my $changes = "ALTER TABLE $name1 DROP FOREIGN KEY $fk;";
396             $changes .= " # was CONSTRAINT $fk $fks1->{$fk}"
397 0 0         unless $self->{opts}{'no-old-defs'};
398 0           $changes .= "\nALTER TABLE $name1 ADD CONSTRAINT $fk FOREIGN KEY $fks2->{$fk};\n";
399 0           push @changes, $changes;
400             }
401             } else {
402 0           debug(1,"foreign key '$fk' removed");
403 0           my $changes .= "ALTER TABLE $name1 DROP FOREIGN KEY $fk;";
404             $changes .= " # was CONSTRAINT $fk $fks1->{$fk}"
405 0 0         unless $self->{opts}{'no-old-defs'};
406 0           $changes .= "\n";
407 0           push @changes, $changes;
408             }
409             }
410             }
411              
412 0 0         if($fks2) {
413 0           for my $fk (keys %$fks2) {
414 0 0 0       next if($fks1 && $fks1->{$fk});
415 0           debug(1, "foreign key '$fk' added");
416 0           push @changes, "ALTER TABLE $name1 ADD CONSTRAINT $fk FOREIGN KEY $fks2->{$fk};\n";
417             }
418             }
419              
420 0           return @changes;
421             }
422              
423             # If we're about to drop a composite (multi-column) index, we need to
424             # check whether any of the columns in the composite index are
425             # auto_increment; if so, we have to add an index for that
426             # auto_increment column *before* dropping the composite index, since
427             # auto_increment columns must always be indexed.
428             sub _check_for_auto_col {
429 0     0     my ($table, $fields, $primary) = @_;
430              
431 0           my @fields = _fields_from_key($fields);
432              
433 0           for my $field (@fields) {
434 0 0         next if($table->field($field) !~ /auto_increment/i);
435 0 0         next if($table->isa_index($field));
436 0 0 0       next if($primary && $table->isa_primary($field));
437              
438 0           return $field;
439             }
440              
441 0           return;
442             }
443              
444             sub _fields_from_key {
445 0     0     my $key = shift;
446 0           $key =~ s/^\s*\((.*)\)\s*$/$1/g; # strip brackets if any
447 0           split /\s*,\s*/, $key;
448             }
449              
450             sub _key_covers_auto_col {
451 0     0     my ($table, $key) = @_;
452 0           my @fields = _fields_from_key($key);
453 0           for my $field (@fields) {
454 0 0         return 1 if $table->is_auto_inc($field);
455             }
456 0           return;
457             }
458              
459             sub _index_auto_col {
460 0     0     my ($table, $field) = @_;
461 0           my $name = $table->name;
462 0           return "ALTER TABLE $name ADD INDEX ($field); # auto columns must always be indexed\n";
463             }
464              
465             sub _diff_options {
466 0     0     my ($self, $table1, $table2) = @_;
467              
468 0           my $name = $table1->name();
469 0           my $options1 = $table1->options();
470 0           my $options2 = $table2->options();
471              
472 0 0 0       return () unless $options1 || $options2;
473              
474 0           my @changes;
475              
476 0 0         if ($self->{opts}{tolerant}) {
477 0           for ($options1, $options2) {
478 0           s/ AUTO_INCREMENT=\d+//gi;
479 0           s/ COLLATE=[\w_]+//gi;
480             }
481             }
482              
483 0 0         if ($options1 ne $options2) {
484 0           my $change = "ALTER TABLE $name $options2;";
485 0 0 0       $change .= " # was " . ($options1 || 'blank') unless $self->{opts}{'no-old-defs'};
486 0           $change .= "\n";
487 0           push @changes, $change;
488             }
489              
490 0           return @changes;
491             }
492              
493             sub _load_database {
494 0     0     my ($self, $arg, $authnum) = @_;
495              
496 0           debug(2, "parsing arg $authnum: '$arg'\n");
497              
498 0           my %auth;
499 0           for my $auth (qw/dbh host port user password socket/) {
500 0   0       $auth{$auth} = $self->{opts}{"$auth$authnum"} || $self->{opts}{$auth};
501 0 0         delete $auth{$auth} unless $auth{$auth};
502             }
503              
504 0 0         if ($arg =~ /^db:(.*)/) {
505 0           return MySQL::Diff::Database->new(db => $1, auth => \%auth, 'single-transaction' => $self->{opts}{'single-transaction'}, 'table-re' => $self->{opts}{'table-re'});
506             }
507              
508 0 0 0       if ($self->{opts}{"dbh"} ||
      0        
      0        
      0        
      0        
509             $self->{opts}{"host$authnum"} ||
510             $self->{opts}{"port$authnum"} ||
511             $self->{opts}{"user$authnum"} ||
512             $self->{opts}{"password$authnum"} ||
513             $self->{opts}{"socket$authnum"}) {
514 0           return MySQL::Diff::Database->new(db => $arg, auth => \%auth, 'single-transaction' => $self->{opts}{'single-transaction'}, 'table-re' => $self->{opts}{'table-re'});
515             }
516              
517 0 0         if (-f $arg) {
518 0           return MySQL::Diff::Database->new(file => $arg, auth => \%auth, 'single-transaction' => $self->{opts}{'single-transaction'}, 'table-re' => $self->{opts}{'table-re'});
519             }
520              
521 0           my %dbs = MySQL::Diff::Database::available_dbs(%auth);
522 0           debug(2, " available databases: ", (join ', ', keys %dbs), "\n");
523              
524 0 0         if ($dbs{$arg}) {
525 0           return MySQL::Diff::Database->new(db => $arg, auth => \%auth, 'single-transaction' => $self->{opts}{'single-transaction'}, 'table-re' => $self->{opts}{'table-re'});
526             }
527              
528 0           warn "'$arg' is not a valid file or database.\n";
529 0           return;
530             }
531              
532             sub _debug_level {
533 0     0     my ($self,$level) = @_;
534 0           debug_level($level);
535             }
536              
537             1;
538              
539             __END__