diff --git a/bin/mysqldiff b/bin/mysqldiff index 92eb175..7286dcb 100755 --- a/bin/mysqldiff +++ b/bin/mysqldiff @@ -77,6 +77,10 @@ non-interactively patch database1 to match database2 enable debugging [level N, default 1] +=item C<-e, --events> + +include mysql events + =item C<-l, --list-tables> output the list off all used tables @@ -93,6 +97,10 @@ don't output DROP TABLE commands don't output DROP COLUMN commands +=item C<-E, --keep-old-events> + +don't output DROP EVENT commands + =item C<-n, --no-old-defs> suppress comments describing old definitions @@ -206,9 +214,9 @@ use String::ShellQuote qw(shell_quote); use MySQL::Diff; my %opts = (); -GetOptions(\%opts, "help|?", "debug|d:i", "apply|A", "batch-apply|B", - "keep-old-tables|k", "keep-old-columns|c", "no-old-defs|n", - "only-both|o", "table-re|t=s", +GetOptions(\%opts, "help|?", "debug|d:i", "events|e", "apply|A", "batch-apply|B", + "keep-old-tables|k", "keep-old-columns|c", "keep-old-events|E", + "no-old-defs|n", "only-both|o", "table-re|t=s", "host|h=s", "port|P=s", "socket|s=s", "user|u=s", "password|p:s", "host1=s", "port1=s", "socket1=s", "user1=s", "password1:s", "host2=s", "port2=s", "socket2=s", "user2=s", "password2:s", @@ -245,10 +253,12 @@ Options: -A, --apply interactively patch database1 to match database2 -B, --batch-apply non-interactively patch database1 to match database2 -d, --debug[=N] enable debugging [level N, default 1] + -e, --events output mysql events -l, --list-tables output the list off all used tables -o, --only-both only output changes for tables in both databases -k, --keep-old-tables don't output DROP TABLE commands -c, --keep-old-columns don't output DROP COLUMN commands + -E, --keep-old-events don't output DROP EVENT commands -n, --no-old-defs suppress comments describing old definitions -t, --table-re=REGEXP restrict comparisons to tables matching REGEXP -i, --tolerant ignore DEFAULT, AUTO_INCREMENT, COLLATE, and formatting changes diff --git a/lib/MySQL/Diff.pm b/lib/MySQL/Diff.pm index 691a9ab..80c5c08 100644 --- a/lib/MySQL/Diff.pm +++ b/lib/MySQL/Diff.pm @@ -61,7 +61,7 @@ sub new { if($hash{debug}) { debug_level($hash{debug}) ; delete $hash{debug}; } if($hash{debug_file}) { debug_file($hash{debug_file}) ; delete $hash{debug_file}; } - debug(3,"\nconstructing new MySQL::Diff"); + debug(3,"\nconstructing new MySQL::Diff, opts: @{[%hash]}"); return $self; } @@ -113,38 +113,118 @@ the schema of the first database into that of the second. sub diff { my $self = shift; my @changes; + my %unsorted_changes; my %used_tables = (); debug(1, "\ncomparing databases"); + for my $table1 ($self->db1->tables()) { + my @diffs; my $name = $table1->name(); + my $parents = $table1->parents(); $used_tables{'-- '. $name} = 1; debug(4, "table 1 $name = ".Dumper($table1)); debug(2,"looking at tables called '$name'"); if (my $table2 = $self->db2->table_by_name($name)) { debug(3,"comparing tables called '$name'"); - push @changes, $self->_diff_tables($table1, $table2); + push @diffs, $self->_diff_tables($table1, $table2); + # push @changes, $diffs; } else { debug(3,"table '$name' dropped"); - push @changes, "DROP TABLE $name;\n\n" - unless $self->{opts}{'only-both'} || $self->{opts}{'keep-old-tables'}; + push @diffs, "DROP TABLE $name;\n\n" + unless $self->{opts}{'only-both'} || $self->{opts}{'keep-old-tables'}; + # push @changes, $diffs + # unless $self->{opts}{'only-both'} || $self->{opts}{'keep-old-tables'}; } + $unsorted_changes{$name}{'diffs'} = [@diffs]; + $unsorted_changes{$name}{'parents'}=$parents; } for my $table2 ($self->db2->tables()) { + my @diffs; my $name = $table2->name(); + my $parents = $table2->parents(); $used_tables{'-- '. $name} = 1; debug(4, "table 2 $name = ".Dumper($table2)); if (! $self->db1->table_by_name($name)) { debug(3,"table '$name' added"); debug(4,"table '$name' added '".$table2->def()."'"); - push @changes, $table2->def() . "\n" - unless $self->{opts}{'only-both'}; + push @diffs, $table2->def() . "\n" + unless $self->{opts}{'only-both'}; + # push @changes, $diffs + # unless $self->{opts}{'only-both'}; } + push @{$unsorted_changes{$name}{'diffs'}},@diffs; + $unsorted_changes{$name}{'parents'}=$parents; } - debug(4,join '', @changes); + debug(1,"Unsorted_changes: ".Dumper(%unsorted_changes)); + + # Sort for Parents + my %checked_changes; + debug(1,"Start sorting for parental constraints"); + foreach my $t (keys %unsorted_changes) { + debug(2,"Checking table: ".$t); + push @changes, add($t); + } + + sub add { + my $table = $_[0]; + + if (exists $checked_changes{$table}) { + debug(5,"table ".$table." in sorted hash, skipping"); + return; + }else{ + debug(5,"table ".$table." not in sorted hash, adding"); + } + + if (exists $unsorted_changes{$table}{'parents'}) { + debug(5, $table." has parents, checking"); + }else{ + debug(5, $table." has no parents, returning"); + $checked_changes{$table} = "done"; + return @{$unsorted_changes{$table}{'diffs'}}; + } + + my @tmparray; + foreach my $parent (keys %{$unsorted_changes{$table}{'parents'}}) { + debug(5,"Doing parent table: ".$parent." of ".$table); + push @tmparray, add($parent); + } + debug(5,"Done with parents, proceeding to table: ".$table); + $checked_changes{$table} = "done"; + push @tmparray, @{$unsorted_changes{$table}{'diffs'}}; + return @tmparray; + } + debug(1,"Finished sorting for parental constraints"); + + for my $event1 ($self->db1->events()) { + my $name = $event1->name(); + debug(4, "event 1 $name = ".Dumper($event1)); + debug(2,"looking at events called '$name'"); + if (my $event2 = $self->db2->event_by_name($name)) { + debug(3,"comparing events called '$name'"); + push @changes, $self->_diff_events($event1, $event2); + } else { + debug(3,"event '$name' dropped"); + push @changes, "DROP EVENT $name;\n\n" + unless $self->{opts}{'only-both'} || $self->{opts}{'keep-old-events'}; + } + } + + for my $event2 ($self->db2->events()) { + my $name = $event2->name(); + debug(4, "event 2 $name = ".Dumper($event2)); + if (! $self->db1->event_by_name($name)) { + debug(3,"event '$name' added"); + debug(4,"event '$name' added '".$event2->def()."'"); + push @changes, $event2->def() . "\n" + unless $self->{opts}{'only-both'}; + } + } + + debug(1,join '', @changes); my $out = ''; if (@changes) { @@ -172,14 +252,14 @@ sub _diff_banner { my $opt_text = join ', ', - map { $self->{opts}{$_} eq '1' ? $_ : "$_=$self->{opts}{$_}" } + map { $self->{opts}{$_} eq '1' ? $_ : "$_=$self->{opts}{$_}" unless $_ eq "password" } keys %{$self->{opts}}; $opt_text = "## Options: $opt_text\n" if $opt_text; my $now = scalar localtime(); return <_diff_foreign_key_drop(@_), $self->_diff_fields(@_), $self->_diff_indices(@_), + $self->_diff_partitions(@_), $self->_diff_primary_key(@_), $self->_diff_foreign_key_add(@_), - $self->_diff_options(@_) + $self->_diff_options(@_) ); $changes[-1] =~ s/\n*$/\n/ if (@changes); @@ -211,23 +292,33 @@ sub _diff_fields { my $fields1 = $table1->fields; my $fields2 = $table2->fields; + my $charset1 = $table1->charset; + my $charset2 = $table2->charset; + + my $collate1 = $table1->collate; + my $collate2 = $table2->collate; + return () unless $fields1 || $fields2; my @changes; - + if($fields1) { for my $field (keys %$fields1) { debug(3,"table1 had field '$field'"); my $f1 = $fields1->{$field}; my $f2 = $fields2->{$field}; if ($fields2 && $f2) { - if ($self->{opts}{tolerant}) { - for ($f1, $f2) { - s/ COLLATE [\w_]+//gi; - } - } + debug(10,"F1 was field '$f1'"); + $f1 =~ s/ CHARACTER SET ${charset1}//gi; + $f1 =~ s/ COLLATE ${collate1}//gi; + debug(10,"F1 now field '$f1'"); + debug(10,"F2 was field '$f2'"); + $f2 =~ s/ CHARACTER SET ${charset2}//gi; + $f2 =~ s/ COLLATE ${collate2}//gi; + debug(10,"F2 now field '$f2'"); + if ($f1 ne $f2) { - if (not $self->{opts}{tolerant} or + if (not $self->{opts}{tolerant} or (($f1 !~ m/$f2\(\d+,\d+\)/) and ($f1 ne "$f2 DEFAULT '' NOT NULL") and ($f1 ne "$f2 NOT NULL") )) @@ -285,16 +376,19 @@ sub _diff_indices { if($indices1) { for my $index (keys %$indices1) { debug(3,"table1 had index '$index'"); - my $old_type = $table1->is_unique($index) ? 'UNIQUE' : + my $old_type = $table1->is_unique($index) ? 'UNIQUE' : + $table1->is_spatial($index) ? 'SPATIAL INDEX' : $table1->is_fulltext($index) ? 'FULLTEXT INDEX' : 'INDEX'; if ($indices2 && $indices2->{$index}) { if( ($indices1->{$index} ne $indices2->{$index}) or ($table1->is_unique($index) xor $table2->is_unique($index)) or + ($table1->is_spatial($index) xor $table2->is_spatial($index)) or ($table1->is_fulltext($index) xor $table2->is_fulltext($index)) ) { debug(3,"index '$index' changed"); - my $new_type = $table2->is_unique($index) ? 'UNIQUE' : + my $new_type = $table2->is_unique($index) ? 'UNIQUE' : + $table2->is_spatial($index) ? 'SPATIAL INDEX' : $table2->is_fulltext($index) ? 'FULLTEXT INDEX' : 'INDEX'; my $changes = "ALTER TABLE $name1 DROP INDEX $index;"; @@ -325,10 +419,121 @@ sub _diff_indices { _key_covers_auto_col($table2, $index) ); debug(3,"index '$index' added"); - my $new_type = $table2->is_unique($index) ? 'UNIQUE' : 'INDEX'; + my $new_type = $table2->is_unique($index) ? 'UNIQUE' : + $table2->is_spatial($index) ? 'SPATIAL INDEX' : 'INDEX'; push @changes, "ALTER TABLE $name1 ADD $new_type $index ($indices2->{$index});\n"; } } + return @changes; +} + +sub _diff_events { + my ($self, $event1, $event2) = @_; + + my $name1 = $event1->name(); + + my @changes; + + debug(3,"event1 '$event1'"); + debug(3,"event2 '$event2'"); + + my $schedule_changed = 0; + my $enable_changed = 0; + my $preserve_changed = 0; + my $body_changed = 0; + + if($event1->schedule() ne $event2->schedule()) { + debug(3, "schedule changed"); + $schedule_changed = 1; + } + + if($event1->preserve() ne $event2->preserve()) { + debug(3, "preserve changed"); + $preserve_changed = 1; + } + + if($event1->enable() ne $event2->enable()) { + debug(3, "enable changed"); + $enable_changed = 1; + } + + if($event1->body() ne $event2->body()) { + debug(3, "body changed"); + $body_changed = 1; + } + + if($schedule_changed or $preserve_changed or $enable_changed or $body_changed) { + my $change = "DELIMITER ;;\nALTER EVENT $name1"; + + if($schedule_changed) { + $change .= " ON SCHEDULE $event2->{schedule}"; + } + + if($preserve_changed) { + $change .= " ON COMPLETION $event2->{preserve}"; + } + + if($enable_changed) { + $change .= " $event2->{enable}"; + } + + if($body_changed) { + $change .= " DO $event2->{body}"; + } + + $change .= ";; \nDELIMITER ;"; + + push @changes, $change; + } + + return @changes; +} + +sub _diff_partitions { + my ($self, $table1, $table2) = @_; + + my $name1 = $table1->name(); + + my $partitions1 = $table1->partitions(); + my $partitions2 = $table2->partitions(); + + return () unless $partitions1 || $partitions2; + + my @changes; + + if($partitions1) { + for my $partition (keys %$partitions1) { + debug(3,"table1 had partition '$partition'"); + if ($partitions2 && $partitions2->{$partition}){ + if( ($partitions1->{$partition}{val} ne $partitions2->{$partition}{val}) or + ($partitions1->{$partition}{op} ne $partitions2->{$partition}{op})){ + debug(3,"partition '$partition' for values '$partitions1->{$partition}{op}' '$partitions1->{$partition}{val}' changed"); + my $changes = "ALTER TABLE $name1 DROP PARTITION $partition;"; + $changes .= " # was VALUES '$partitions1->{$partition}{op}' '$partitions1->{$partition}{val}'" + unless $self->{opts}{'no-old-defs'}; + $changes .= "\nALTER TABLE $name1 ADD PARTITION (PARTITION $partition VALUES $partitions2->{$partition}{op} ($partitions2->{$partition}{val}));\n"; + push @changes, $changes; + } + } else { + # ALTER TABLE t1 DROP PARTITION p0, p1; + debug(3,"partition '$partition' for values '$partitions1->{$partition}{op}' '$partitions1->{$partition}{val}' removed"); + my $changes = "ALTER TABLE $name1 DROP PARTITION $partition;"; + $changes .= " # was VALUES '$partitions1->{$partition}{op}' '$partitions1->{$partition}{val}'" + unless $self->{opts}{'no-old-defs'}; + $changes .= "\n"; + push @changes, $changes; + } + } + } + + # ALTER TABLE t1 ADD PARTITION (PARTITION p3 VALUES LESS THAN (2002)); + if($partitions2) { + for my $partition (keys %$partitions2) { + next if($partitions1 && $partitions1->{$partition}); + debug(3,"partition '$partition' for values '$partitions2->{$partition}{op}' '$partitions2->{$partition}{val}' added"); + push @changes, "ALTER TABLE $name1 ADD PARTITION (PARTITION $partition VALUES $partitions2->{$partition}{op} ($partitions2->{$partition}{val}));\n"; + } + } return @changes; } @@ -344,7 +549,7 @@ sub _diff_primary_key { return () unless $primary1 || $primary2; my @changes; - + if ($primary1 && ! $primary2) { debug(3,"primary key '$primary1' dropped"); my $changes = _index_auto_col($table2, $primary1); @@ -384,24 +589,24 @@ sub _diff_foreign_key_drop { return () unless $fks1 || $fks2; my @changes; - + if($fks1) { for my $fk (keys %$fks1) { debug(1,"$name1 has fk '$fk'"); if ($fks2 && $fks2->{$fk}) { - if($fks1->{$fk} ne $fks2->{$fk}) + if($fks1->{$fk}->{'value'} ne $fks2->{$fk}->{'value'}) { debug(1,"foreign key '$fk' changed"); - my $changes = "ALTER TABLE $name1 DROP FOREIGN KEY $fk;"; - $changes .= " # was CONSTRAINT $fk $fks1->{$fk}" + my $changes = "ALTER TABLE $name1 DROP FOREIGN KEY $fks1->{$fk}->{'name'};"; + $changes .= " # was CONSTRAINT $fk $fks1->{$fk}->{'value'}" unless $self->{opts}{'no-old-defs'}; push @changes, $changes; } } else { debug(1,"foreign key '$fk' removed"); - my $changes .= "ALTER TABLE $name1 DROP FOREIGN KEY $fk;"; - $changes .= " # was CONSTRAINT $fk $fks1->{$fk}" + my $changes .= "ALTER TABLE $name1 DROP FOREIGN KEY $fks1->{$fk}->{'name'};"; + $changes .= " # was CONSTRAINT $fk $fks1->{$fk}->{'value'}" unless $self->{opts}{'no-old-defs'}; $changes .= "\n"; push @changes, $changes; @@ -429,13 +634,14 @@ sub _diff_foreign_key_add { debug(1,"$name1 has fk '$fk'"); if ($fks2 && $fks2->{$fk}) { - if($fks1->{$fk} ne $fks2->{$fk}) + if($fks1->{$fk}->{'value'} ne $fks2->{$fk}->{'value'}) { debug(1,"foreign key '$fk' changed"); - my $changes = "\nALTER TABLE $name1 ADD CONSTRAINT $fk FOREIGN KEY $fks2->{$fk};\n"; + my $changes = "\nALTER TABLE $name1 ADD CONSTRAINT $fk FOREIGN KEY $fks2->{$fk}->{'value'};\n"; push @changes, $changes; } } + } } @@ -443,7 +649,7 @@ sub _diff_foreign_key_add { for my $fk (keys %$fks2) { next if($fks1 && $fks1->{$fk}); debug(1, "foreign key '$fk' added"); - push @changes, "ALTER TABLE $name1 ADD CONSTRAINT $fk FOREIGN KEY $fks2->{$fk};\n"; + push @changes, "ALTER TABLE $name1 ADD CONSTRAINT $fk FOREIGN KEY $fks2->{$fk}->{'value'};\n"; } } @@ -505,6 +711,7 @@ sub _diff_options { if ($self->{opts}{tolerant}) { for ($options1, $options2) { + s/ CHARACTER SET [\w_]+//gi; s/ AUTO_INCREMENT=\d+//gi; s/ COLLATE=[\w_]+//gi; } @@ -541,18 +748,18 @@ sub _load_database { $self->{opts}{"user$authnum"} || $self->{opts}{"password$authnum"} || $self->{opts}{"socket$authnum"}) { - return MySQL::Diff::Database->new(db => $arg, auth => \%auth, 'single-transaction' => $self->{opts}{'single-transaction'}, 'table-re' => $self->{opts}{'table-re'}); + return MySQL::Diff::Database->new(db => $arg, auth => \%auth, 'single-transaction' => $self->{opts}{'single-transaction'}, 'table-re' => $self->{opts}{'table-re'}, events => $self->{opts}{events}); } if (-f $arg) { - return MySQL::Diff::Database->new(file => $arg, auth => \%auth, 'single-transaction' => $self->{opts}{'single-transaction'}, 'table-re' => $self->{opts}{'table-re'}); + return MySQL::Diff::Database->new(file => $arg, auth => \%auth, 'single-transaction' => $self->{opts}{'single-transaction'}, 'table-re' => $self->{opts}{'table-re'}, events => $self->{opts}{events}); } my %dbs = MySQL::Diff::Database::available_dbs(%auth); debug(2, " available databases: ", (join ', ', keys %dbs), "\n"); if ($dbs{$arg}) { - return MySQL::Diff::Database->new(db => $arg, auth => \%auth, 'single-transaction' => $self->{opts}{'single-transaction'}, 'table-re' => $self->{opts}{'table-re'}); + return MySQL::Diff::Database->new(db => $arg, auth => \%auth, 'single-transaction' => $self->{opts}{'single-transaction'}, 'table-re' => $self->{opts}{'table-re'}, events => $self->{opts}{events}); } warn "'$arg' is not a valid file or database.\n"; diff --git a/lib/MySQL/Diff/Database.pm b/lib/MySQL/Diff/Database.pm index ba558de..16e5ff4 100644 --- a/lib/MySQL/Diff/Database.pm +++ b/lib/MySQL/Diff/Database.pm @@ -14,6 +14,8 @@ MySQL::Diff::Database - Database Definition Class my $name = $db->name(); my @tables = $db->tables(); my $table_def = $db->table_by_name($table); + my @events = $db->events(); + my $event_def = $db->event_by_name($event); my @dbs = MySQL::Diff::Database::available_dbs(); @@ -38,6 +40,7 @@ use IO::File; use MySQL::Diff::Utils qw(debug); use MySQL::Diff::Table; +use MySQL::Diff::Event; # ------------------------------------------------------------------------------ @@ -71,6 +74,7 @@ sub new { $self->{_source}{dbh} = $p{dbh} if $p{dbh}; $self->{'single-transaction'} = $p{'single-transaction'}; $self->{'table-re'} = $p{'table-re'}; + $self->{events} = $p{events}; if ($p{file}) { $self->_canonicalise_file($p{file}); @@ -109,7 +113,7 @@ Provides a summary of the database. sub summary { my $self = shift; - + if ($self->{_source}{file}) { return "file: " . $self->{_source}{file}; } elsif ($self->{_source}{db}) { @@ -155,7 +159,29 @@ Returns the table definition (see L) for the given table. sub table_by_name { my ($self,$name) = @_; - return $self->{_by_name}{$name}; + return $self->{_tables_by_name}{$name}; +} + +=item * events() + +Returns a list of events for the current database. + +=cut + +sub events { + my $self = shift; + return @{$self->{_events}}; +} + +=item * event_by_name() + +Returns the event definition (see L) for the given event. + +=cut + +sub event_by_name { + my ($self,$name) = @_; + return $self->{_events_by_name}{$name}; } =back @@ -177,8 +203,8 @@ Note that is used as a function call, not a method call. sub available_dbs { my %auth = @_; my $args_ref = _auth_args_string(%auth); - unshift @$args_ref, q{mysqlshow}; - + unshift @$args_ref, q{mysqlshow}; + # evil but we don't use DBI because I don't want to implement -p properly # not that this works with -p anyway ... my $command = shell_quote @$args_ref; @@ -211,16 +237,17 @@ sub _canonicalise_file { # hopefully the temp db is unique! my $temp_db = sprintf "test_mysqldiff-temp-%d_%d_%d", time(), $$, rand(); debug(3,"creating temporary database $temp_db"); - + my $defs = read_file($file); die "$file contains dangerous command '$1'; aborting.\n" if $defs =~ /;\s*(use|((drop|create)\s+database))\b/i; - + my $args = $self->{_source}{auth}; my $fh = IO::File->new("| mysql $args") or die "Couldn't execute 'mysql$args': $!\n"; print $fh "\nCREATE DATABASE \`$temp_db\`;\nUSE \`$temp_db\`;\n"; print $fh $defs; - $fh->close; + # looks like this was never caught + $fh->close or die "Could not apply schema from $file"; # ... and then retrieve defs from mysqldump. Hence we've used # MySQL to massage the defs file into canonical form. @@ -283,6 +310,7 @@ sub _get_defs { my ( $self, $db ) = @_; my $args = $self->{_source}{auth}; + my $events = $self->{events} ? "--events" : ""; my $single_transaction = $self->{'single-transaction'} ? "--single-transaction" : ""; my $tables = ''; #dump all tables by default if ( my $table_re = $self->{'table-re'} ) { @@ -293,10 +321,10 @@ sub _get_defs { } } - my $fh = IO::File->new("mysqldump -d $single_transaction $args $db $tables 2>&1 |") + my $fh = IO::File->new("mysqldump -d $events $single_transaction $args $db $tables 2>&1 |") or die "Couldn't read ${db}'s table defs via mysqldump: $!\n"; - debug( 3, "running mysqldump -d $single_transaction $args $db $tables" ); + debug( 3, "running mysqldump -d $events $single_transaction $args $db $tables" ); my $defs = $self->{_defs} = [<$fh>]; $fh->close; my $exit_status = $? >> 8; @@ -318,21 +346,57 @@ EOF sub _parse_defs { my $self = shift; + debug(2, "parse_defs: @{$self->{_defs}}"); + + my $table_defs = join '', grep ! /^\s*(\#|SET|\/\*\!\d{5}\sSET)/, @{$self->{_defs}}; + $table_defs =~ s/`//sg; + # Delete all event defs (from this match onwards) + $table_defs =~ s/-- Dumping events.*\n.*//sg; + # Delete any other comments + $table_defs =~ s/^--.*$//mg; + # Delete empty lines + $table_defs =~ s/^\s*$//mg; + + if(!$self->{_tables}) { + debug(2, "parsing table defs"); + debug(4, " defs [$table_defs]"); + my @tables = split /(?=^\s*(?:create|alter|drop)\s+table\s+)/im, $table_defs; + + debug(4, " tables [@tables]"); + $self->{_tables} = []; + for my $table (@tables) { + debug(4, " table def [$table]"); + if($table =~ /create\s+table/i) { + my $obj = MySQL::Diff::Table->new(source => $self->{_source}, def => $table); + push @{$self->{_tables}}, $obj; + $self->{_tables_by_name}{$obj->name()} = $obj; + } + } + } - return if $self->{_tables}; - - debug(2, "parsing table defs"); - my $defs = join '', grep ! /^\s*(\#|--|SET|\/\*)/, @{$self->{_defs}}; - $defs =~ s/`//sg; - my @tables = split /(?=^\s*(?:create|alter|drop)\s+table\s+)/im, $defs; - $self->{_tables} = []; - for my $table (@tables) { - debug(4, " table def [$table]"); - if($table =~ /create\s+table/i) { - my $obj = MySQL::Diff::Table->new(source => $self->{_source}, def => $table); - push @{$self->{_tables}}, $obj; - $self->{_by_name}{$obj->name()} = $obj; - } + my $event_defs = join '', grep ! /^\s*(\#|\/\*\!\d{5}\sSET)/, @{$self->{_defs}}; + # Delete all table defs: ie from start until this match + $event_defs =~ s/^.*-- Dumping events/-- Dumping events/sg; + # Or any other comment + $event_defs =~ s/^--.*$//mg; + + if(!$self->{_events}) { + debug(2, "parsing event defs"); + debug(4, " defs [$event_defs]"); + my @events = split /(?=^.*(?:drop).*event.*)/im, $event_defs; + + debug(4, " events [@events]"); + $self->{_events} = []; + for my $event (@events) { + debug(4, " event def [$event]"); + # events are always dumped with a preceding `DROP EVENT IF EXISTS` to the `CREATE EVENT` statements + # we need to capture it too in the def due to DELIMITER shenanigans + if($event =~ /.*drop.*event.*/i) { + my $obj = MySQL::Diff::Event->new(source => $self->{_source}, def => $event); + push @{$self->{_events}}, $obj; + $self->{_events_by_name}{$obj->name()} = $obj; + } + } } } diff --git a/lib/MySQL/Diff/Event.pm b/lib/MySQL/Diff/Event.pm new file mode 100644 index 0000000..2996cf8 --- /dev/null +++ b/lib/MySQL/Diff/Event.pm @@ -0,0 +1,114 @@ +package MySQL::Diff::Event; + +=head1 NAME + +MySQL::Diff::Event - Event Definition Class + +=head1 SYNOPSIS + + use MySQL::Diff::Event + + my $db = MySQL::Diff::Event->new(%options); + my $def = $db->def(); + my $name = $db->name(); + my $definer = $db->definer(); + +=head1 DESCRIPTION + +Parses an event definition into component parts. + +=cut + +use warnings; +use strict; + +our $VERSION = '0.60'; + +# ------------------------------------------------------------------------------ +# Libraries + +use Carp qw(:DEFAULT); +use MySQL::Diff::Utils qw(debug); + +# ------------------------------------------------------------------------------ + +=head1 METHODS + +=head2 Constructor + +=over 4 + +=item new( %options ) + +Instantiate the objects, providing the command line options for database +access and process requirements. + +=cut + +sub new { + my $class = shift; + my %hash = @_; + my $self = {}; + bless $self, ref $class || $class; + + $self->{$_} = $hash{$_} for(keys %hash); + + debug(3,"\nconstructing new MySQL::Diff::Event"); + croak "MySQL::Diff::Event::new called without def params" unless $self->{def}; + $self->_parse; + return $self; +} + +=back + +=head2 Public Methods + +Fuller documentation will appear here in time :) + +=over 4 + +=item * def + +Returns the event definition as a string. + +=item * name + +Returns the name of the current event. + +=back + +=cut + +sub def { my $self = shift; return $self->{def}; } +sub name { my $self = shift; return $self->{name}; } +sub schedule { my $self = shift; return $self->{schedule}; } +sub preserve { my $self = shift; return $self->{preserve}; } +sub enable { my $self = shift; return $self->{enable}; } +sub body { my $self = shift; return $self->{body}; } +# +# ------------------------------------------------------------------------------ +# Private Methods + +sub _parse { + my $self = shift; + + $self->{def} =~ s/\n+/\n/; + $self->{lines} = [ grep ! /^\s*$/, split /(?=^)/m, $self->{def} ]; + my @lines = @{$self->{lines}}; + debug(4,"parsing event def: '$self->{def}'"); + + my $name; + my $all_lines = join "\n", @lines; + if ($all_lines =~ /^\/\*!\d{5}\sCREATE\*\/\s\/\*!\d{5}\sDEFINER=(\S+)\*\/\s\/\*!\d{5}\sEVENT\s`(\w+)`\sON SCHEDULE (.*)\sON COMPLETION (PRESERVE|NOT PRESERVE)\s(ENABLE|DISABLE)\sDO\s(.*?)\s\*\//ms) { + $self->{definer} = $1; + $self->{name} = $2; + $self->{schedule} = $3; + $self->{preserve} = $4; + $self->{enable} = $5; + $self->{body} = $6; + debug(3,"got event name '$self->{name}'"); + shift @lines; + } else { + croak "couldn't figure out event name"; + } +} diff --git a/lib/MySQL/Diff/Table.pm b/lib/MySQL/Diff/Table.pm index 894656a..79c5748 100644 --- a/lib/MySQL/Diff/Table.pm +++ b/lib/MySQL/Diff/Table.pm @@ -8,20 +8,27 @@ MySQL::Diff::Table - Table Definition Class use MySQL::Diff::Table - my $db = MySQL::Diff::Database->new(%options); - my $def = $db->def(); - my $name = $db->name(); - my $field = $db->field(); - my $fields = $db->fields(); # %$fields - my $primary_key = $db->primary_key(); - my $indices = $db->indices(); # %$indices - my $options = $db->options(); - - my $isfield = $db->isa_field($field); - my $isprimary = $db->isa_primary($field); - my $isindex = $db->isa_index($field); - my $isunique = $db->is_unique($field); - my $isfulltext = $db->is_fulltext($field); + my $table = MySQL::Diff::Table->new(%options); + my $def = $table->def(); + my $name = $table->name(); + my $field = $table->field(); + my $fields = $table->fields(); # %$fields + my $primary_key = $table->primary_key(); + my $indices = $table->indices(); # %$indices + my $parents = $table->parents(); # %$parents + my $partitions = $table->partitions(); # %$partitions + my $options = $table->options(); + my $engine = $table->engine(); + my $charset = $table->charset(); + my $collate = $table->collate(); + + my $isfield = $table->isa_field($field); + my $isprimary = $table->isa_primary($field); + my $isindex = $table->isa_index($field); + my $isunique = $table->is_unique($field); + my $isspatial = $table->is_spatial($field); + my $isfulltext = $table->is_fulltext($field); + my $ipatitioned = $table->is_paritioned($field); =head1 DESCRIPTION @@ -101,10 +108,30 @@ Returns a hash reference to fields used as primary key fields. Returns a hash reference to fields used as index fields. +=item * parents + +Returns a hash reference to fields used as parents. + +=item * partitions + +Returns a hash reference to fields used as partitions. + =item * options Returns the additional options added to the table definition. +=item * engine + +Returns the additional engine table option added to the table definition. + +=item * character set + +Returns the additional character set table option added to the table definition. + +=item * collate + +Returns the additional collate table option added to the table definition. + =item * isa_field Returns 1 if given field is used in the current table definition, otherwise @@ -122,6 +149,10 @@ Returns 1 if given field is used as an index field, otherwise returns 0. Returns 1 if given field is used as unique index field, otherwise returns 0. +=item * is_spatial + +Returns 1 if given field is used as spatial index field, otherwise returns 0. + =item * is_fulltext Returns 1 if given field is used as fulltext index field, otherwise returns 0. @@ -130,6 +161,10 @@ Returns 1 if given field is used as fulltext index field, otherwise returns 0. Returns 1 if given field is defined as an auto increment field, otherwise returns 0. +=item * is_paritioned + +Returns if given field is a paritioned field + =back =cut @@ -140,16 +175,23 @@ sub field { my $self = shift; return $self->{fields}{$_[0]}; } sub fields { my $self = shift; return $self->{fields}; } sub primary_key { my $self = shift; return $self->{primary_key}; } sub indices { my $self = shift; return $self->{indices}; } +sub parents { my $self = shift; return $self->{parents}; } +sub partitions { my $self = shift; return $self->{partitions}; } sub options { my $self = shift; return $self->{options}; } sub foreign_key { my $self = shift; return $self->{foreign_key}; } +sub engine { my $self = shift; return $self->{engine}; } +sub charset { my $self = shift; return $self->{charset}; } +sub collate { my $self = shift; return $self->{collate}; } sub isa_field { my $self = shift; return $_[0] && $self->{fields}{$_[0]} ? 1 : 0; } sub isa_primary { my $self = shift; return $_[0] && $self->{primary}{$_[0]} ? 1 : 0; } sub isa_index { my $self = shift; return $_[0] && $self->{indices}{$_[0]} ? 1 : 0; } sub is_unique { my $self = shift; return $_[0] && $self->{unique}{$_[0]} ? 1 : 0; } +sub is_spatial { my $self = shift; return $_[0] && $self->{spatial}{$_[0]} ? 1 : 0; } sub is_fulltext { my $self = shift; return $_[0] && $self->{fulltext}{$_[0]} ? 1 : 0; } sub is_auto_inc { my $self = shift; return $_[0] && $self->{auto_inc}{$_[0]} ? 1 : 0; } +sub is_partitioned { my $self = shift; return $_[0] && $self->{partitions}{$_[0]} ? 1 : 0; } # ------------------------------------------------------------------------------ # Private Methods @@ -186,23 +228,42 @@ sub _parse { next; } - + if (/^(?:CONSTRAINT\s+(.*)?)?\s+FOREIGN\s+KEY\s+(.*)$/) { - my ($key, $val) = ($1, $2); - croak "foreign key '$key' duplicated in table '$name'\n" - if $self->{foreign_key}{$key}; - debug(1,"got foreign key $key"); - $self->{foreign_key}{$key} = $val; - next; + my $val = $2; + if (/^(?:CONSTRAINT\s+(.*)?)?\s+FOREIGN\s+KEY\s+\((.+?)\)\sREFERENCES\s(.+?)\s\((.+?)\)(.*)/) { + my ($const_name, $const_local_column, $const_parent_table, $const_parent_column, $const_options) = ($1, $2, $3, $4, $5); + debug(1,"new foreign key $const_local_column-$const_parent_table-$const_parent_column"); + my $key = "$self->{name}_${const_local_column}_${const_parent_table}_${const_parent_column}"; + + $self->{parents}{$const_parent_table} = $key; + croak "foreign key '$key' duplicated in table '$name'\n" + if $self->{foreign_key}{$key}; + debug(1,"got foreign key $key"); + $self->{foreign_key}{$key}{value} = $val; + $self->{foreign_key}{$key}{name} = $const_name; + next; + } } if (/^(KEY|UNIQUE(?: KEY)?)\s+(\S+?)(?:\s+USING\s+(?:BTREE|HASH|RTREE))?\s*\((.*)\)(?:\s+USING\s+(?:BTREE|HASH|RTREE))?$/) { my ($type, $key, $val) = ($1, $2, $3); - croak "index '$key' duplicated in table '$self->{name}'\n" - if $self->{indices}{$key}; + my $indexName = $val; + $indexName =~ tr/,/_/; + $self->{indices}{$indexName} = $val; + $self->{unique}{$indexName} = 1 if($type =~ /unique/i); + debug(4, "got ", defined $self->{unique}{$indexName} ? 'unique ' : '', "index key '$indexName': ($val)"); + next; + } + + if (/^(SPATIAL(?:\s+KEY|INDEX)?)\s+(\S+?)\s*\((.*)\)$/) { + my ($type, $key, $val) = ($1, $2, $3); + debug(4, "type: $type key: $key val: $val"); + croak "SPATIAL index '$key' duplicated in table '$self->{name}'\n" + if $self->{fulltext}{$key}; $self->{indices}{$key} = $val; - $self->{unique}{$key} = 1 if($type =~ /unique/i); - debug(4, "got ", defined $self->{unique}{$key} ? 'unique ' : '', "index key '$key': ($val)"); + $self->{spatial}{$key} = 1; + debug(4,"got SPATIAL index '$key': ($val)"); next; } @@ -216,10 +277,62 @@ sub _parse { next; } - if (/^\)\s*(.*?);$/) { # end of table definition + if (/^\)\s*(.*?)(;?)$/) { # end of table definition $self->{options} = $1; - debug(4,"got table options '$self->{options}'"); - last; + my $ending = $2; + if (/^\)\s*ENGINE=([^\s;]+).*\s+DEFAULT CHARSET=([^\s;]+)\s+COLLATE=([^\s;]+)/) { + $self->{engine} = $1; + $self->{charset} = $2; + $self->{collate} = $3; + debug(4,"options contained $1 $2 $3"); + } else { + debug(1,"no regexp match for option content"); + } + if ($ending){ # there is a ; at the end + debug(4,"got table options '$self->{options}'"); + last; + } + debug(4,"got table options '$self->{options}' but no end ';'"); + next; + } + + if ($self->{options}) { + # option is set, but wait, there is more to this schema... e.g. a patition? + # + # got field def '/*!50100': PARTITION BY RANGE (HOUR(timestamp)) ' + if(/^\/\*\!\d{5}\sPARTITION\sBY\s(\S+?)\s\((.+)\)/){ + my ($func, $opt) = ($1, $2); + debug(4," got partition function:'$func' with op: '$opt'"); + $self->{partition}{function} = $func; + $self->{partition}{option} = $opt; + next; + } + if($self->{partition}{function} eq "RANGE"){ + if(/^\(?PARTITION (\S+?) VALUES (\S+?) THAN \(*(.*?)\)?\sENGINE = InnoDB(.*)/){ + my ($name, $op, $val, $term) = ($1, "$2 THAN", $3, $4); + debug(4," got extended partition table options name:'$name' op: '$op' val: '$val' "); + $self->{partitions}{$name}{val} = $val; + $self->{partitions}{$name}{op} = $op; + if ($term =~ m/;/) { + debug(4," got last section - ending"); + last; + } + next; + } + } + if($self->{partition}{function} eq "LIST"){ + if(/^\(?PARTITION (\S+?) VALUES IN \(*(.*?)\)?\sENGINE = InnoDB(.*)/){ + my ($name, $op, $val, $term) = ($1, "IN", $2, $3); + debug(4," got extended partition table options name:'$name' op: '$op' val: '$val' "); + $self->{partitions}{$name}{val} = $val; + $self->{partitions}{$name}{op} = $op; + if ($term =~ m/;/) { + debug(4," got last section - ending"); + last; + } + next; + } + } # we can add other functions here such as hash... etc. } if (/^(\S+)\s*(.*)/) { diff --git a/lib/MySQL/Diff/Utils.pm b/lib/MySQL/Diff/Utils.pm index e247a10..f2ec5cc 100644 --- a/lib/MySQL/Diff/Utils.pm +++ b/lib/MySQL/Diff/Utils.pm @@ -88,10 +88,10 @@ is equal to or lower than the current debug level. return; } } - + print STDERR @_,"\n"; } - + } 1; diff --git a/t/all.t b/t/all.t index 478eb2b..ba5346a 100644 --- a/t/all.t +++ b/t/all.t @@ -125,6 +125,162 @@ CREATE TABLE qux ( ) DEFAULT CHARACTER SET utf8; ', + zap1 => ' +CREATE TABLE zap ( + polygons multipolygon NOT NULL +) DEFAULT CHARACTER SET utf8; +', + + zap2 => ' +CREATE TABLE zap ( + polygons multipolygon NOT NULL, + SPATIAL INDEX idx_polygons (polygons) +) DEFAULT CHARACTER SET utf8; +', + + pip1 => ' +CREATE TABLE pip ( + id VARCHAR(255) NOT NULL, + timestamp DATETIME(3) NOT NULL, + PRIMARY KEY(id,timestamp) +) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin + PARTITION BY RANGE (HOUR(timestamp)) ( + PARTITION p0 VALUES LESS THAN (1), + PARTITION p1 VALUES LESS THAN (2), + PARTITION p2 VALUES LESS THAN (3), + PARTITION p3 VALUES LESS THAN (4), + PARTITION p4 VALUES LESS THAN (5), + PARTITION p5 VALUES LESS THAN (6), + PARTITION p6 VALUES LESS THAN (7), + PARTITION p7 VALUES LESS THAN (8), + PARTITION p8 VALUES LESS THAN (9), + PARTITION p9 VALUES LESS THAN (10), + PARTITION p10 VALUES LESS THAN (11), + PARTITION p11 VALUES LESS THAN (12), + PARTITION p12 VALUES LESS THAN (13), + PARTITION p13 VALUES LESS THAN (14), + PARTITION p14 VALUES LESS THAN (15), + PARTITION p15 VALUES LESS THAN (16), + PARTITION p16 VALUES LESS THAN (17), + PARTITION p17 VALUES LESS THAN (18), + PARTITION p18 VALUES LESS THAN (19), + PARTITION p19 VALUES LESS THAN (20), + PARTITION p20 VALUES LESS THAN (21), + PARTITION p21 VALUES LESS THAN (22), + PARTITION p22 VALUES LESS THAN MAXVALUE +); +', + + pip2 => ' +CREATE TABLE pip ( + id VARCHAR(255) NOT NULL, + timestamp DATETIME(3) NOT NULL, + PRIMARY KEY(id,timestamp) +) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin + PARTITION BY RANGE (HOUR(timestamp)) ( + PARTITION p0 VALUES LESS THAN (1), + PARTITION p1 VALUES LESS THAN (2), + PARTITION p2 VALUES LESS THAN (3), + PARTITION p3 VALUES LESS THAN (4), + PARTITION p4 VALUES LESS THAN (5), + PARTITION p5 VALUES LESS THAN (6), + PARTITION p6 VALUES LESS THAN (7), + PARTITION p7 VALUES LESS THAN (8), + PARTITION p8 VALUES LESS THAN (9), + PARTITION p9 VALUES LESS THAN (10), + PARTITION p10 VALUES LESS THAN (11), + PARTITION p11 VALUES LESS THAN (12), + PARTITION p12 VALUES LESS THAN (13), + PARTITION p13 VALUES LESS THAN (14), + PARTITION p14 VALUES LESS THAN (15), + PARTITION p15 VALUES LESS THAN (16), + PARTITION p16 VALUES LESS THAN (17), + PARTITION p17 VALUES LESS THAN (18), + PARTITION p18 VALUES LESS THAN (19), + PARTITION p19 VALUES LESS THAN (20), + PARTITION p20 VALUES LESS THAN (21), + PARTITION p21 VALUES LESS THAN (22), + PARTITION p22 VALUES LESS THAN (23), + PARTITION p23 VALUES LESS THAN MAXVALUE +); +', + pip3 => ' +CREATE TABLE pip ( + id VARCHAR(255) NOT NULL, + timestamp DATETIME(3) NOT NULL, + PRIMARY KEY(id,timestamp) +) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin; +', + piq1 => ' +CREATE TABLE piq ( + id VARCHAR(255) NOT NULL, + timestamp DATETIME(3) NOT NULL, + PRIMARY KEY(id,timestamp) +) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin + PARTITION BY LIST (DAYOFWEEK(`timestamp`)) ( + PARTITION p1 VALUES IN (1) ENGINE = InnoDB, + PARTITION p2 VALUES IN (2) ENGINE = InnoDB, + PARTITION p3 VALUES IN (3) ENGINE = InnoDB, + PARTITION p4 VALUES IN (4) ENGINE = InnoDB, + PARTITION p5 VALUES IN (5) ENGINE = InnoDB, + PARTITION p6 VALUES IN (6) ENGINE = InnoDB +); +', + + piq2 => ' +CREATE TABLE piq ( + id VARCHAR(255) NOT NULL, + timestamp DATETIME(3) NOT NULL, + PRIMARY KEY(id,timestamp) +) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin + PARTITION BY LIST (DAYOFWEEK(`timestamp`)) ( + PARTITION p1 VALUES IN (1) ENGINE = InnoDB, + PARTITION p2 VALUES IN (2) ENGINE = InnoDB, + PARTITION p3 VALUES IN (3) ENGINE = InnoDB, + PARTITION p4 VALUES IN (4) ENGINE = InnoDB, + PARTITION p5 VALUES IN (5) ENGINE = InnoDB, + PARTITION p6 VALUES IN (6) ENGINE = InnoDB, + PARTITION p7 VALUES IN (7) ENGINE = InnoDB +); +', + piq3 => ' +CREATE TABLE piq ( + id VARCHAR(255) NOT NULL, + timestamp DATETIME(3) NOT NULL, + PRIMARY KEY(id,timestamp) +) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin; +', + evt1 => ' +CREATE TABLE evt ( + id INT(11) NOT NULL auto_increment, + foreign_id INT(11) NOT NULL, + PRIMARY KEY (id) +) DEFAULT CHARACTER SET utf8; +', + evt2 => ' +CREATE TABLE evt ( + id INT(11) NOT NULL auto_increment, + foreign_id INT(11) NOT NULL, + PRIMARY KEY (id) +) DEFAULT CHARACTER SET utf8; +DELIMITER ;; +CREATE EVENT example_event ON SCHEDULE EVERY 1 DAY STARTS \'2020-01-01 00:00:00\' ON COMPLETION PRESERVE ENABLE DO BEGIN + CALL backup_script(); + END ;; +DELIMITER ; +', + evt3 => ' +CREATE TABLE evt ( + id INT(11) NOT NULL auto_increment, + foreign_id INT(11) NOT NULL, + PRIMARY KEY (id) +) DEFAULT CHARACTER SET utf8; +DELIMITER ;; +CREATE EVENT example_event ON SCHEDULE EVERY 2 DAY STARTS \'2020-01-01 01:00:00\' ON COMPLETION NOT PRESERVE DISABLE DO BEGIN + CALL another_backup_script(); + END ;; +DELIMITER ; +', ); my %tests = ( @@ -202,11 +358,11 @@ ALTER TABLE foo DROP COLUMN field; ALTER TABLE foo ADD COLUMN field blob; CREATE TABLE bar ( - id int(11) NOT NULL auto_increment, + id int NOT NULL auto_increment, ctime datetime default NULL, utime datetime default NULL, name char(16) default NULL, - age int(11) default NULL, + age int default NULL, PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; @@ -334,7 +490,6 @@ ALTER TABLE foo ADD PRIMARY KEY (id,foreign_id); ALTER TABLE foo DROP INDEX id; ', ], - 'drop additional primary key' => [ {}, @@ -369,7 +524,6 @@ ALTER TABLE foo DROP INDEX id; ALTER TABLE bar ADD UNIQUE name (name,age); ', ], - 'drop index' => [ {}, @@ -385,7 +539,6 @@ ALTER TABLE bar ADD UNIQUE name (name,age); ALTER TABLE bar DROP INDEX name; # was UNIQUE (name,age) ', ], - 'alter indices' => [ {}, @@ -517,6 +670,159 @@ ALTER TABLE qux ADD COLUMN id int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY; ALTER TABLE qux ADD COLUMN id int(11) NOT NULL AUTO_INCREMENT UNIQUE KEY; ', ], + + 'add spatial index' => + [ + {}, + $tables{zap1}, + $tables{zap2}, + '## mysqldiff +## +## Run on +## +## --- file: tmp.db1 +## +++ file: tmp.db2 + +ALTER TABLE zap ADD SPATIAL INDEX idx_polygons (polygons); +', + ], + + 'remove spatial index' => + [ + {}, + $tables{zap2}, + $tables{zap1}, + '## mysqldiff +## +## Run on +## +## --- file: tmp.db1 +## +++ file: tmp.db2 + +ALTER TABLE zap DROP INDEX idx_polygons; # was SPATIAL INDEX (polygons) +', + ], + + 'Add range partition' => + [ + {}, + $tables{pip1}, + $tables{pip2}, + '## mysqldiff +## +## Run on +## +## --- file: tmp.db1 +## +++ file: tmp.db2 + +ALTER TABLE pip DROP PARTITION p22; # was VALUES \'LESS THAN\' \'MAXVALUE\' +ALTER TABLE pip ADD PARTITION (PARTITION p22 VALUES LESS THAN (23)); +ALTER TABLE pip ADD PARTITION (PARTITION p23 VALUES LESS THAN (MAXVALUE)); +', + ], + 'remove range partition' => + [ + {}, + $tables{pip2}, + $tables{pip1}, + '## mysqldiff +## +## Run on +## +## --- file: tmp.db1 +## +++ file: tmp.db2 + +ALTER TABLE pip DROP PARTITION p23; # was VALUES \'LESS THAN\' \'MAXVALUE\' +ALTER TABLE pip DROP PARTITION p22; # was VALUES \'LESS THAN\' \'23\' +ALTER TABLE pip ADD PARTITION (PARTITION p22 VALUES LESS THAN (MAXVALUE)); +', + ], + 'Add list partition' => + [ + {}, + $tables{piq1}, + $tables{piq2}, + '## mysqldiff +## +## Run on +## +## --- file: tmp.db1 +## +++ file: tmp.db2 + +ALTER TABLE piq ADD PARTITION (PARTITION p7 VALUES IN (7)); +', + ], + 'remove list partition' => + [ + {}, + $tables{piq2}, + $tables{piq1}, + '## mysqldiff +## +## Run on +## +## --- file: tmp.db1 +## +++ file: tmp.db2 + +ALTER TABLE piq DROP PARTITION p7; # was VALUES \'IN\' \'7\' +', + ], + 'add event' => + [ + {}, + $tables{evt1}, + $tables{evt2}, + '## mysqldiff +## +## Run on +## +## --- file: tmp.db1 +## +++ file: tmp.db2 + +/*!50106 DROP EVENT IF EXISTS `example_event` */; +DELIMITER ;; +/*!50106 CREATE*/ /*!50117 DEFINER=`root`@`localhost`*/ /*!50106 EVENT `example_event` ON SCHEDULE EVERY 1 DAY STARTS \'2024-01-01 00:00:00\' ON COMPLETION PRESERVE ENABLE DO BEGIN + CALL backup_script(); + END */ ;; +DELIMITER ; +', + ], + 'drop event' => + [ + {}, + $tables{evt2}, + $tables{evt1}, + '## mysqldiff +## +## Run on +## +## --- file: tmp.db1 +## +++ file: tmp.db2 + +DROP EVENT example_event; +', + ], + 'alter event' => + [ + {}, + $tables{evt2}, + $tables{evt3}, + '## mysqldiff +## +## Run on +## +## --- file: tmp.db1 +## +++ file: tmp.db2 + +DELIMITER ;; +ALTER EVENT example_event ON SCHEDULE EVERY 2 DAY STARTS \'2024-01-01 00:00:00\' ON COMPLETION NOT PRESERVE DISABLE DO BEGIN + + CALL another_backup_script(); + + END;; +DELIMITER ; +', + ] ); my $BAIL = check_setup();