[ Index ] |
|
Code source de osCommerce 2.2ms2-060817 |
1 #!/usr/bin/perl -w 2 # 3 # mysqldiff 4 # 5 # Utility to compare table definitions in two MySQL databases, 6 # and output a patch in the format of ALTER TABLE statements 7 # which converts the first database structure into in the second. 8 # 9 # Developed as part of the http://www.guideguide.com/ project. 10 # If you like hacking Perl in a cool environment, come and work for us! 11 # 12 # See http://www.new.ox.ac.uk/~adam/computing/mysqldiff/ for the 13 # latest version. 14 # 15 # Copyright (c) 2000 Adam Spiers <adam@spiers.net>. All rights 16 # reserved. This program is free software; you can redistribute it 17 # and/or modify it under the same terms as Perl itself. 18 # 19 20 use strict; 21 22 use vars qw($VERSION); 23 $VERSION = '0.25'; 24 25 require 5.004; 26 27 use Carp qw(:DEFAULT cluck); 28 use FindBin qw($Script); 29 use Getopt::Long; 30 31 my %opts = (); 32 GetOptions(\%opts, "help|?", "debug|d:i", 33 "no-old-defs|n", "only-both|o", "table-re|t=s", 34 "host|h=s", "user|u=s", "password|p:s", 35 "host1|h1=s", "user1|u1=s", "password1|p1:s", 36 "host2|h2=s", "user2|u2=s", "password2|p2:s", 37 "tolerant|i" 38 ); 39 40 if (@ARGV != 2 or $opts{help}) { 41 usage(); 42 exit 1; 43 } 44 45 $opts{debug}++ if exists $opts{debug} && $opts{debug} == 0; 46 my $debug = $opts{debug} || 0; 47 48 my $table_re; 49 $table_re = qr/$opts{'table-re'}/ if $opts{'table-re'}; 50 51 my @db = (); 52 for my $num (0, 1) { 53 $db[$num] = parse_arg($ARGV[$num], $num); 54 } 55 56 diff_dbs(@db); 57 58 exit 0; 59 60 ############################################################################## 61 # 62 63 sub usage { 64 print STDERR @_, "\n" if @_; 65 die <<EOF; 66 Usage: $Script [ options ] <database1> <database2> 67 68 Options: 69 -?, --help show this help 70 -d, --debug[=N] enable debugging [level N, default 1] 71 -o, --only-both only output changes for tables in both databases 72 -n, --no-old-defs suppress comments describing old definitions 73 -t, --table-re=REGEXP restrict comparisons to tables matching REGEXP 74 -i, --tolerant ignore DEFAULT and formatting changes 75 76 -h, --host=... connect to host 77 -u, --user=... user for login if not current user 78 -p, --password[=...] password to use when connecting to server 79 80 for <databaseN> only, where N == 1 or 2, 81 -hN, --hostN=... connect to host 82 -uN, --userN=... user for login if not current user 83 -pN, --passwordN[=...] password to use when connecting to server 84 85 Databases can be either files or database names. 86 If there is an ambiguity, the file will be preferred; 87 to prevent this prefix the database argument with `db:'. 88 EOF 89 } 90 91 sub diff_dbs { 92 my @db = @_; 93 94 debug(1, "comparing databases\n"); 95 96 my @changes = (); 97 98 foreach my $table1 ($db[0]->tables()) { 99 my $name = $table1->name(); 100 if ($table_re && $name !~ $table_re) { 101 debug(2, " table `$name' didn't match $opts{'table-re'}; ignoring\n"); 102 next; 103 } 104 debug(2, " looking at tables called `$name'\n"); 105 if (my $table2 = $db[1]->table_by_name($name)) { 106 debug(4, " comparing tables called `$name'\n"); 107 push @changes, diff_tables($table1, $table2); 108 } 109 else { 110 debug(3, " table `$name' dropped\n"); 111 push @changes, "DROP TABLE $name;\n\n" 112 unless $opts{'only-both'}; 113 } 114 } 115 116 foreach my $table2 ($db[1]->tables()) { 117 my $name = $table2->name(); 118 if ($table_re && $name !~ $table_re) { 119 debug(2, " table `$name' matched $opts{'table-re'}; ignoring\n"); 120 next; 121 } 122 if (! $db[0]->table_by_name($name)) { 123 debug(3, " table `$name' added\n"); 124 push @changes, $table2->def() . "\n" 125 unless $opts{'only-both'}; 126 } 127 } 128 129 if (@changes) { 130 diff_banner(@db); 131 print @changes; 132 } 133 } 134 135 sub diff_banner { 136 my @db = @_; 137 138 my $summary1 = $db[0]->summary(); 139 my $summary2 = $db[1]->summary(); 140 141 my $now = scalar localtime(); 142 print <<EOF; 143 ## mysqldiff $VERSION 144 ## 145 ## run on $now 146 ## 147 ## --- $summary1 148 ## +++ $summary2 149 150 EOF 151 } 152 153 sub diff_tables { 154 my @changes = (diff_fields(@_), 155 diff_indices(@_), 156 diff_primary_key(@_)); 157 if (@changes) { 158 $changes[-1] .= "\n"; 159 } 160 return @changes; 161 } 162 163 sub diff_fields { 164 my ($table1, $table2) = @_; 165 166 my $name1 = $table1->name(); 167 168 my %fields1 = %{ $table1->fields() }; 169 my %fields2 = %{ $table2->fields() }; 170 171 my @changes = (); 172 173 foreach my $field (keys %fields1) { 174 my $f1 = $fields1{$field}; 175 if (my $f2 = $fields2{$field}) { 176 if ($f1 ne $f2) { 177 if (not $opts{tolerant} or (($f1 !~ m/$f2\(\d+,\d+\)/) and 178 ($f1 ne "$f2 DEFAULT '' NOT NULL") and 179 ($f1 ne "$f2 NOT NULL") 180 )) 181 { 182 debug(4, " field `$field' changed\n"); 183 184 my $change = "ALTER TABLE $name1 CHANGE COLUMN $field $field $f2;"; 185 $change .= " # was $f1" unless $opts{'no-old-defs'}; 186 $change .= "\n"; 187 push @changes, $change; 188 } 189 } 190 } 191 else { 192 debug(4, " field `$field' removed\n"); 193 my $change = "ALTER TABLE $name1 DROP COLUMN $field;"; 194 $change .= " # was $fields1{$field}" unless $opts{'no-old-defs'}; 195 $change .= "\n"; 196 push @changes, $change; 197 } 198 } 199 200 foreach my $field (keys %fields2) { 201 if (! $fields1{$field}) { 202 debug(4, " field `$field' added\n"); 203 push @changes, "ALTER TABLE $name1 ADD COLUMN $field $fields2{$field};\n"; 204 } 205 } 206 207 return @changes; 208 } 209 210 sub diff_indices { 211 my ($table1, $table2) = @_; 212 213 my $name1 = $table1->name(); 214 215 my %indices1 = %{ $table1->indices() }; 216 my %indices2 = %{ $table2->indices() }; 217 218 my @changes = (); 219 220 foreach my $index (keys %indices1) { 221 my $old_type = $table1->is_unique_index($index) ? 'UNIQUE' : 'INDEX'; 222 223 if ($indices2{$index}) { 224 if ($indices1{$index} ne $indices2{$index} || 225 ($table1->is_unique_index($index) 226 xor 227 $table2->is_unique_index($index))) 228 { 229 debug(4, " index `$index' changed\n"); 230 my $new_type = $table2->is_unique_index($index) ? 'UNIQUE' : 'INDEX'; 231 232 my $changes = ''; 233 if ($indices1{$index}) { 234 $changes .= "ALTER TABLE $name1 DROP INDEX $index;"; 235 $changes .= " # was $old_type ($indices1{$index})" unless $opts{'no-old-defs'}; 236 $changes .= "\n"; 237 } 238 239 $changes .= <<EOF; 240 ALTER TABLE $name1 ADD $new_type $index ($indices2{$index}); 241 EOF 242 push @changes, $changes; 243 } 244 } 245 else { 246 debug(4, " index `$index' removed\n"); 247 my $change = "ALTER TABLE $name1 DROP INDEX $index;"; 248 $change .= " # was $old_type ($indices1{$index})" unless $opts{'no-old-defs'}; 249 $change .= "\n"; 250 push @changes, $change; 251 } 252 } 253 254 foreach my $index (keys %indices2) { 255 if (! $indices1{$index}) { 256 debug(4, " index `$index' added\n"); 257 push @changes, 258 "ALTER TABLE $name1 ADD INDEX $index ($indices2{$index});\n"; 259 } 260 } 261 262 return @changes; 263 } 264 265 sub diff_primary_key { 266 my ($table1, $table2) = @_; 267 268 my $name1 = $table1->name(); 269 270 my $primary1 = $table1->primary_key(); 271 my $primary2 = $table2->primary_key(); 272 273 my @changes = (); 274 if (($primary1 xor $primary2) || ($primary1 && ($primary1 ne $primary2))) { 275 debug(4, " primary key changed\n"); 276 my $change = "ALTER TABLE $name1 DROP PRIMARY KEY;"; 277 $change .= " # was ($primary1)" unless $opts{'no-old-defs'}; 278 $change .= <<EOF; 279 280 ALTER TABLE $name1 ADD PRIMARY KEY ($primary2); 281 EOF 282 push @changes, $change; 283 } 284 285 return @changes; 286 } 287 288 ############################################################################## 289 290 sub auth_args { 291 my %auth = @_; 292 my $args = ''; 293 for my $arg (qw/host user password/) { 294 $args .= " --$arg=$auth{$arg}" if $auth{$arg}; 295 } 296 return $args; 297 } 298 299 sub available_dbs { 300 my %auth = @_; 301 my $args = auth_args(%auth); 302 303 # evil but we don't use DBI because I don't want to implement -p properly 304 # not that this works with -p anyway ... 305 open(MYSQLSHOW, "mysqlshow$args |") 306 or die "Couldn't execute `mysqlshow$args': $!\n"; 307 my @dbs = (); 308 while (<MYSQLSHOW>) { 309 next unless /^\| (\w+)/; 310 push @dbs, $1; 311 } 312 close(MYSQLSHOW); 313 314 return map { $_ => 1 } @dbs; 315 } 316 317 sub parse_arg { 318 my ($arg, $num) = @_; 319 320 debug(1, "parsing arg $num: `$arg'\n"); 321 322 my $authnum = $num + 1; 323 324 my %auth = (); 325 for my $auth (qw/host user password/) { 326 $auth{$auth} = $opts{"$auth$authnum"} || $opts{$auth}; 327 delete $auth{$auth} unless $auth{$auth}; 328 } 329 330 if ($arg =~ /^db:(.*)/) { 331 return new MySQL::Database(db => $1, %auth); 332 } 333 334 if ($opts{"host$authnum"} || 335 $opts{"user$authnum"} || 336 $opts{"password$authnum"}) 337 { 338 return new MySQL::Database(db => $arg, %auth); 339 } 340 341 if (-e $arg) { 342 return new MySQL::Database(file => $arg, %auth); 343 } 344 345 my %dbs = available_dbs(%auth); 346 debug(2, " available databases: ", (join ', ', keys %dbs), "\n"); 347 348 if ($dbs{$arg}) { 349 return new MySQL::Database(db => $arg, %auth); 350 } 351 352 usage("`$arg' is not a valid file or database.\n"); 353 exit 1; 354 } 355 356 sub debug { 357 my $level = shift; 358 print STDERR @_ if $debug >= $level && @_; 359 } 360 361 ############################################################################## 362 # 363 364 package MySQL::Database; 365 366 use Carp qw(:DEFAULT cluck); 367 368 sub debug { &::debug } 369 370 sub new { 371 my $class = shift; 372 my %p = @_; 373 my $self = {}; 374 bless $self, ref $class || $class; 375 376 debug(2, " constructing new MySQL::Database\n"); 377 378 my $args = &::auth_args(%p); 379 debug(3, " auth args: $args\n"); 380 381 if ($p{file}) { 382 $self->{_source} = { file => $p{file} }; 383 debug(3, " fetching table defs from file $p{file}\n"); 384 385 # FIXME: option to avoid create-and-dump bit 386 # create a temporary database using defs from file ... 387 # hopefully the temp db is unique! 388 my $temp_db = sprintf "test_mysqldiff_temp_%d_%d", time(), $$; 389 debug(3, " creating temporary database $temp_db\n"); 390 391 open(DEFS, $p{file}) 392 or die "Couldn't open `$p{file}': $!\n"; 393 open(MYSQL, "| mysql $args") 394 or die "Couldn't execute `mysql$args': $!\n"; 395 print MYSQL <<EOF; 396 CREATE DATABASE $temp_db; 397 USE $temp_db; 398 EOF 399 print MYSQL <DEFS>; 400 close(DEFS); 401 close(MYSQL); 402 403 # ... and then retrieve defs from mysqldump. Hence we've used 404 # MySQL to massage the defs file into canonical form. 405 $self->_get_defs($temp_db, $args); 406 407 debug(3, " dropping temporary database $temp_db\n"); 408 open(MYSQL, "| mysql $args") 409 or die "Couldn't execute `mysql$args': $!\n"; 410 print MYSQL "DROP DATABASE $temp_db;\n"; 411 close(MYSQL); 412 } 413 elsif ($p{db}) { 414 $self->{_source} = { db => $p{db}, auth => $args }; 415 debug(3, " fetching table defs from db $p{db}\n"); 416 $self->_get_defs($p{db}, $args); 417 } 418 else { 419 confess "MySQL::Database::new called without db or file params"; 420 } 421 422 $self->_parse_defs(); 423 424 return $self; 425 } 426 427 sub _get_defs { 428 my $self = shift; 429 my ($db, $args) = @_; 430 431 open(MYSQLDUMP, "mysqldump -d $args $db |") 432 or die "Couldn't read $db}'s table defs via mysqldump: $!\n"; 433 debug(3, " running mysqldump -d $args $db\n"); 434 $self->{_defs} = [ <MYSQLDUMP> ]; 435 close(MYSQLDUMP); 436 } 437 438 sub _parse_defs { 439 my $self = shift; 440 441 return if $self->{_tables}; 442 443 debug(3, " parsing table defs\n"); 444 my $defs = join '', grep ! /^\s*(--|\#)/, @{$self->{_defs}}; 445 my @tables = split /(?=^\s*create\s+table\s+)/im, $defs; 446 foreach my $table (@tables) { 447 next unless $table =~ /create\s+table/i; 448 my $obj = MySQL::Table->new(source => $self->{_source}, 449 def => $table); 450 push @{$self->{_tables}}, $obj; 451 $self->{_by_name}{$obj->name()} = $obj; 452 } 453 } 454 455 sub tables { 456 return @{$_[0]->{_tables}}; 457 } 458 459 sub table_by_name { 460 my $self = shift; 461 my ($name) = @_; 462 return $self->{_by_name}{$name}; 463 } 464 465 sub summary { 466 my $self = shift; 467 468 if ($self->{_source}{file}) { 469 return "file: " . $self->{_source}{file}; 470 } 471 elsif ($self->{_source}{db}) { 472 my $args = $self->{_source}{auth}; 473 $args =~ tr/-//d; 474 $args =~ s/\bpassword=\S+//; 475 $args =~ s/^\s*(.*?)\s*$/$1/; 476 my $summary = " db: " . $self->{_source}{db}; 477 $summary .= " ($args)" if $args; 478 return $summary; 479 } 480 else { 481 return 'unknown'; 482 } 483 } 484 485 ############################################################################## 486 # 487 488 package MySQL::Table; 489 490 use Carp qw(:DEFAULT cluck); 491 492 sub debug { &::debug } 493 494 sub new { 495 my $class = shift; 496 my %p = @_; 497 my $self = {}; 498 bless $self, ref $class || $class; 499 500 debug(4, " constructing new MySQL::Table\n"); 501 502 if (! $p{def}) { 503 croak "MySQL::Table::new called without def params"; 504 } 505 506 $self->parse($p{def}); 507 508 $self->{_source} = $p{source}; 509 510 return $self; 511 } 512 513 sub parse { 514 my $self = shift; 515 my ($def) = @_; 516 517 $def =~ s/\n+/\n/; 518 $self->{_def} = $def; 519 $self->{_lines} = [ grep ! /^\s*$/, split /(?=^)/m, $def ]; 520 my @lines = @{$self->{_lines}}; 521 522 debug(5, " parsing table def\n"); 523 524 my $name; 525 if ($lines[0] =~ /^\s*create\s+table\s+(\S+)\s+\(\s*$/i) { 526 $name = $self->{_name} = $1; 527 debug(5, " got table name `$name'\n"); 528 shift @lines; 529 } 530 else { 531 croak "couldn't figure out table name"; 532 } 533 534 while (@lines) { 535 $_ = shift @lines; 536 s/^\s*(.*?),?\s*$/$1/; # trim whitespace and trailing commas 537 if (/^\);$/) { 538 last; 539 } 540 541 if (/^PRIMARY\s+KEY\s+(.+)$/) { 542 my $primary = $1; 543 croak "two primary keys in table `$name': `$primary', `", 544 $self->{_primary_key}, "'\n" 545 if $self->{_primary_key}; 546 $self->{_primary_key} = $primary; 547 debug(6, " got primary key `$primary'\n"); 548 next; 549 } 550 551 if (/^(KEY|UNIQUE)\s+(\S+?)\s+\((.*)\)$/) { 552 my ($type, $key, $val) = ($1, $2, $3); 553 croak "index `$key' duplicated in table `$name'\n" 554 if $self->{_indices}{$key}; 555 $self->{_indices}{$key} = $val; 556 $self->{_unique_index}{$key} = ($type =~ /unique/i) ? 1 : 0; 557 debug(6, " got ", 558 ($type =~ /unique/i) ? 'unique ' : '', 559 "index key `$key': ($val)\n"); 560 next; 561 } 562 563 if (/^(\S+)\s*(.*)/) { 564 my ($field, $def) = ($1, $2); 565 croak "definition for field `$field' duplicated in table `$name'\n" 566 if $self->{_fields}{$field}; 567 $self->{_fields}{$field} = $def; 568 debug(6, " got field def `$field': $def\n"); 569 next; 570 } 571 572 croak "unparsable line in definition for table `$name':\n$_"; 573 } 574 575 if (@lines) { 576 my $name = $self->name(); 577 warn "table `$name' had trailing garbage:\n", join '', @lines; 578 } 579 } 580 581 sub def { $_[0]->{_def} } 582 sub name { $_[0]->{_name} } 583 sub source { $_[0]->{_source} } 584 sub fields { $_[0]->{_fields} || {} } 585 sub indices { $_[0]->{_indices} || {} } 586 sub primary_key { $_[0]->{_primary_key} } 587 sub is_unique_index { $_[0]->{_unique_index}{$_[1]} }
titre
Description
Corps
titre
Description
Corps
titre
Description
Corps
titre
Corps
Généré le : Mon Nov 26 19:48:25 2007 | par Balluche grâce à PHPXref 0.7 |
![]() |