From 0b6d65685dbf666b2098460b1ce7a245f285c5b5 Mon Sep 17 00:00:00 2001 From: Jason Tevnan Date: Thu, 23 Jan 2020 19:30:17 +0100 Subject: [PATCH 01/19] Adds initial spatial index type (#1) * Adds initial spatial index type * I think i added tests? * I was missing a comment --- lib/MySQL/Diff.pm | 8 +++++++- lib/MySQL/Diff/Table.pm | 17 ++++++++++++++++ t/all.t | 45 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/lib/MySQL/Diff.pm b/lib/MySQL/Diff.pm index 88e1266..1de756f 100644 --- a/lib/MySQL/Diff.pm +++ b/lib/MySQL/Diff.pm @@ -259,6 +259,8 @@ sub _diff_fields { $changes .= ' PRIMARY KEY'; } elsif ($table2->is_unique($field)) { $changes .= ' UNIQUE KEY'; + } elsif ($table2->is_spatial($field)) { + $changes .= ' SPATIAL KEY'; } } push @changes, "$changes;\n"; @@ -285,15 +287,18 @@ sub _diff_indices { for my $index (keys %$indices1) { debug(3,"table1 had index '$index'"); 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' : + $table2->is_spatial($index) ? 'SPATIAL INDEX' : $table2->is_fulltext($index) ? 'FULLTEXT INDEX' : 'INDEX'; my $changes = "ALTER TABLE $name1 DROP INDEX $index;"; @@ -324,7 +329,8 @@ 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"; } } diff --git a/lib/MySQL/Diff/Table.pm b/lib/MySQL/Diff/Table.pm index 894656a..2e04dc1 100644 --- a/lib/MySQL/Diff/Table.pm +++ b/lib/MySQL/Diff/Table.pm @@ -21,6 +21,7 @@ MySQL::Diff::Table - Table Definition Class my $isprimary = $db->isa_primary($field); my $isindex = $db->isa_index($field); my $isunique = $db->is_unique($field); + my $isspatial = $db->is_spatial($field); my $isfulltext = $db->is_fulltext($field); =head1 DESCRIPTION @@ -122,6 +123,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. @@ -147,6 +152,7 @@ sub isa_field { my $self = shift; return $_[0] && $self->{fields}{$_[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; } @@ -206,6 +212,17 @@ sub _parse { 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->{spatial}{$key} = 1; + debug(4,"got SPATIAL index '$key': ($val)"); + next; + } + if (/^(FULLTEXT(?:\s+KEY|INDEX)?)\s+(\S+?)\s*\((.*)\)$/) { my ($type, $key, $val) = ($1, $2, $3); croak "FULLTEXT index '$key' duplicated in table '$self->{name}'\n" diff --git a/t/all.t b/t/all.t index 478eb2b..d8d86e6 100644 --- a/t/all.t +++ b/t/all.t @@ -123,6 +123,19 @@ CREATE TABLE qux ( age INT, UNIQUE KEY (id) ) 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; ', ); @@ -515,6 +528,38 @@ ALTER TABLE qux ADD COLUMN id int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY; ## +++ file: tmp.db2 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) ', ], ); From 762415835d94d96c92f941e55ec6efca2725c87f Mon Sep 17 00:00:00 2001 From: Jason Tevnan Date: Thu, 30 Jan 2020 10:51:43 +0100 Subject: [PATCH 02/19] Adding some more debug values for this (#2) * Adding some more debug values for this * Maybe? * make it a bit more concretet * correct variable casting * Adds partition methods to table * Adds diffing in one direction * Works both ways, needs to dedup though * Compares partitions in both directions correctly * Spelling mistakes and regexp greedy * Commands masked as comments for version compatability are included * Keep comments if they are actually commands * More generic end finder for regular and partition ends, added partition function/option finder * first test * Correct tests * Correct tests * doh * Adds test in other direction * Comment unused tests * Comment unused tests - but really --- lib/MySQL/Diff.pm | 49 +++++++++++++++++ lib/MySQL/Diff/Database.pm | 2 +- lib/MySQL/Diff/Table.pm | 50 +++++++++++++++-- t/all.t | 109 +++++++++++++++++++++++++++++++++++++ 4 files changed, 205 insertions(+), 5 deletions(-) diff --git a/lib/MySQL/Diff.pm b/lib/MySQL/Diff.pm index 1de756f..c3c9757 100644 --- a/lib/MySQL/Diff.pm +++ b/lib/MySQL/Diff.pm @@ -193,6 +193,7 @@ sub _diff_tables { my @changes = ( $self->_diff_fields(@_), $self->_diff_indices(@_), + $self->_diff_partitions(@_), $self->_diff_primary_key(@_), $self->_diff_foreign_key(@_), $self->_diff_options(@_) @@ -334,6 +335,54 @@ sub _diff_indices { push @changes, "ALTER TABLE $name1 ADD $new_type $index ($indices2->{$index});\n"; } } + 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}' THAN '$partitions1->{$partition}{val}' changed"); + my $changes = "ALTER TABLE $name1 DROP PARTITION $partition;"; + $changes .= " # was VALUES '$partitions1->{$partition}{op}' THAN '$partitions1->{$partition}{val}'" + unless $self->{opts}{'no-old-defs'}; + $changes .= "\nALTER TABLE $name1 ADD PARTITION (PARTITION $partition VALUES $partitions2->{$partition}{op} THAN ($partitions2->{$partition}{val}));\n"; + push @changes, $changes; + } + } else { + # ALTER TABLE t1 DROP PARTITION p0, p1; + debug(3,"partition '$partition' for values '$partitions1->{$partition}{op}' THAN '$partitions1->{$partition}{val}' removed"); + my $changes = "ALTER TABLE $name1 DROP PARTITION $partition;"; + $changes .= " # was VALUES '$partitions1->{$partition}{op}' THAN '$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}' THAN '$partitions2->{$partition}{val}' added"); + push @changes, "ALTER TABLE $name1 ADD PARTITION (PARTITION $partition VALUES $partitions2->{$partition}{op} THAN ($partitions2->{$partition}{val}));\n"; + } + } return @changes; } diff --git a/lib/MySQL/Diff/Database.pm b/lib/MySQL/Diff/Database.pm index ba558de..41fa156 100644 --- a/lib/MySQL/Diff/Database.pm +++ b/lib/MySQL/Diff/Database.pm @@ -322,7 +322,7 @@ sub _parse_defs { return if $self->{_tables}; debug(2, "parsing table defs"); - my $defs = join '', grep ! /^\s*(\#|--|SET|\/\*)/, @{$self->{_defs}}; + my $defs = join '', grep ! /^\s*(\#|--|SET|\/\*\!\d{5}\sSET)/, @{$self->{_defs}}; $defs =~ s/`//sg; my @tables = split /(?=^\s*(?:create|alter|drop)\s+table\s+)/im, $defs; $self->{_tables} = []; diff --git a/lib/MySQL/Diff/Table.pm b/lib/MySQL/Diff/Table.pm index 2e04dc1..959ae85 100644 --- a/lib/MySQL/Diff/Table.pm +++ b/lib/MySQL/Diff/Table.pm @@ -15,6 +15,7 @@ MySQL::Diff::Table - Table Definition Class my $fields = $db->fields(); # %$fields my $primary_key = $db->primary_key(); my $indices = $db->indices(); # %$indices + my $partitions = $db->partitions(); # %$partitions my $options = $db->options(); my $isfield = $db->isa_field($field); @@ -23,6 +24,7 @@ MySQL::Diff::Table - Table Definition Class my $isunique = $db->is_unique($field); my $isspatial = $db->is_spatial($field); my $isfulltext = $db->is_fulltext($field); + my $ipatitioned = $db->is_paritioned($field); =head1 DESCRIPTION @@ -102,6 +104,10 @@ Returns a hash reference to fields used as primary key fields. Returns a hash reference to fields used as index fields. +=item * partitions + +Returns a hash reference to fields used as partitions. + =item * options Returns the additional options added to the table definition. @@ -135,6 +141,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 fiel is a praritioned field + =back =cut @@ -145,6 +155,7 @@ 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 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}; } @@ -156,6 +167,7 @@ sub is_spatial { my $self = shift; return $_[0] && $self->{spatial}{$_[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 @@ -218,7 +230,7 @@ sub _parse { croak "SPATIAL index '$key' duplicated in table '$self->{name}'\n" if $self->{fulltext}{$key}; $self->{indices}{$key} = $val; - $self->{spatial}{$key} = 1; + $self->{spatial}{$key} = 1; debug(4,"got SPATIAL index '$key': ($val)"); next; } @@ -233,10 +245,40 @@ 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; + if ($2){ # 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, $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; + } + } # we can add other functions here such as hash... etc. } if (/^(\S+)\s*(.*)/) { diff --git a/t/all.t b/t/all.t index d8d86e6..6e4bc15 100644 --- a/t/all.t +++ b/t/all.t @@ -136,6 +136,80 @@ 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; ', ); @@ -560,6 +634,41 @@ ALTER TABLE zap ADD SPATIAL INDEX idx_polygons (polygons); ## +++ file: tmp.db2 ALTER TABLE zap DROP INDEX idx_polygons; # was SPATIAL INDEX (polygons) +', + ], + + 'Add 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 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)); ', ], ); From 291412fa6ce09625a67371b4f8e2855a0e6de3b4 Mon Sep 17 00:00:00 2001 From: Jason Tevnan Date: Thu, 30 Jan 2020 12:32:25 +0100 Subject: [PATCH 03/19] No spatial column datafield --- lib/MySQL/Diff.pm | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/MySQL/Diff.pm b/lib/MySQL/Diff.pm index c3c9757..7e15ce0 100644 --- a/lib/MySQL/Diff.pm +++ b/lib/MySQL/Diff.pm @@ -260,8 +260,6 @@ sub _diff_fields { $changes .= ' PRIMARY KEY'; } elsif ($table2->is_unique($field)) { $changes .= ' UNIQUE KEY'; - } elsif ($table2->is_spatial($field)) { - $changes .= ' SPATIAL KEY'; } } push @changes, "$changes;\n"; From 0f8f224e9e8976dfc56707593e3e4048abcad2a5 Mon Sep 17 00:00:00 2001 From: Jason Tevnan Date: Thu, 7 May 2020 12:20:04 +0200 Subject: [PATCH 04/19] Add list partition type and tests --- lib/MySQL/Diff.pm | 14 ++++---- lib/MySQL/Diff/Table.pm | 15 ++++++++- t/all.t | 73 +++++++++++++++++++++++++++++++++++++++-- 3 files changed, 92 insertions(+), 10 deletions(-) diff --git a/lib/MySQL/Diff.pm b/lib/MySQL/Diff.pm index 7e15ce0..da84922 100644 --- a/lib/MySQL/Diff.pm +++ b/lib/MySQL/Diff.pm @@ -354,18 +354,18 @@ sub _diff_partitions { 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}' THAN '$partitions1->{$partition}{val}' changed"); + 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}' THAN '$partitions1->{$partition}{val}'" + $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} THAN ($partitions2->{$partition}{val}));\n"; + $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}' THAN '$partitions1->{$partition}{val}' removed"); + 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}' THAN '$partitions1->{$partition}{val}'" + $changes .= " # was VALUES '$partitions1->{$partition}{op}' '$partitions1->{$partition}{val}'" unless $self->{opts}{'no-old-defs'}; $changes .= "\n"; push @changes, $changes; @@ -377,8 +377,8 @@ sub _diff_partitions { if($partitions2) { for my $partition (keys %$partitions2) { next if($partitions1 && $partitions1->{$partition}); - debug(3,"partition '$partition' for values '$partitions2->{$partition}{op}' THAN '$partitions2->{$partition}{val}' added"); - push @changes, "ALTER TABLE $name1 ADD PARTITION (PARTITION $partition VALUES $partitions2->{$partition}{op} THAN ($partitions2->{$partition}{val}));\n"; + 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"; } } diff --git a/lib/MySQL/Diff/Table.pm b/lib/MySQL/Diff/Table.pm index 959ae85..da23268 100644 --- a/lib/MySQL/Diff/Table.pm +++ b/lib/MySQL/Diff/Table.pm @@ -268,7 +268,20 @@ sub _parse { } if($self->{partition}{function} eq "RANGE"){ if(/^\(?PARTITION (\S+?) VALUES (\S+?) THAN \(*(.*?)\)?\sENGINE = InnoDB(.*)/){ - my ($name, $op, $val, $term) = ($1, $2, $3, $4); + 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", $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; diff --git a/t/all.t b/t/all.t index 6e4bc15..69623d4 100644 --- a/t/all.t +++ b/t/all.t @@ -210,6 +210,45 @@ CREATE TABLE pip ( 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 p6 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; ', ); @@ -637,7 +676,7 @@ ALTER TABLE zap DROP INDEX idx_polygons; # was SPATIAL INDEX (polygons) ', ], - 'Add partition' => + 'Add range partition' => [ {}, $tables{pip1}, @@ -654,7 +693,7 @@ ALTER TABLE pip ADD PARTITION (PARTITION p22 VALUES LESS THAN (23)); ALTER TABLE pip ADD PARTITION (PARTITION p23 VALUES LESS THAN (MAXVALUE)); ', ], - 'remove partition' => + 'remove range partition' => [ {}, $tables{pip2}, @@ -669,6 +708,36 @@ ALTER TABLE pip ADD PARTITION (PARTITION p23 VALUES LESS THAN (MAXVALUE)); 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' ', ], ); From 21ddf58afc1329d54ecd5a71f0f3b9ab2e882963 Mon Sep 17 00:00:00 2001 From: Jason Tevnan Date: Thu, 7 May 2020 15:12:23 +0200 Subject: [PATCH 05/19] Corrected order of arguments and updated tests --- lib/MySQL/Diff/Table.pm | 4 ++-- t/all.t | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/MySQL/Diff/Table.pm b/lib/MySQL/Diff/Table.pm index da23268..1f8ea35 100644 --- a/lib/MySQL/Diff/Table.pm +++ b/lib/MySQL/Diff/Table.pm @@ -268,7 +268,7 @@ sub _parse { } if($self->{partition}{function} eq "RANGE"){ if(/^\(?PARTITION (\S+?) VALUES (\S+?) THAN \(*(.*?)\)?\sENGINE = InnoDB(.*)/){ - my ($name, $op, $val, $term) = ($1, "'$2' THAN", $3, $4); + 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; @@ -281,7 +281,7 @@ sub _parse { } if($self->{partition}{function} eq "LIST"){ if(/^\(?PARTITION (\S+?) VALUES IN \(*(.*?)\)?\sENGINE = InnoDB(.*)/){ - my ($name, $op, $val, $term) = ($1, "IN", $3, $4); + 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; diff --git a/t/all.t b/t/all.t index 69623d4..58f8a0b 100644 --- a/t/all.t +++ b/t/all.t @@ -240,7 +240,7 @@ CREATE TABLE piq ( PARTITION p4 VALUES IN (4) ENGINE = InnoDB, PARTITION p5 VALUES IN (5) ENGINE = InnoDB, PARTITION p6 VALUES IN (6) ENGINE = InnoDB, - PARTITION p6 VALUES IN (7) ENGINE = InnoDB + PARTITION p7 VALUES IN (7) ENGINE = InnoDB ); ', piq3 => ' @@ -688,7 +688,7 @@ ALTER TABLE zap DROP INDEX idx_polygons; # was SPATIAL INDEX (polygons) ## --- file: tmp.db1 ## +++ file: tmp.db2 -ALTER TABLE pip DROP PARTITION p22; # was VALUES \'LESS\' THAN \'MAXVALUE\' +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)); ', @@ -705,8 +705,8 @@ ALTER TABLE pip ADD PARTITION (PARTITION p23 VALUES LESS THAN (MAXVALUE)); ## --- 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 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)); ', ], @@ -737,7 +737,7 @@ ALTER TABLE piq ADD PARTITION (PARTITION p7 VALUES IN (7)); ## --- file: tmp.db1 ## +++ file: tmp.db2 -ALTER TABLE piq DROP PARTITION p7; # was VALUES 'IN' '7' +ALTER TABLE piq DROP PARTITION p7; # was VALUES \'IN\' \'7\' ', ], ); From 1bc2c4991bf03f27851a21372fb40cd192f01e21 Mon Sep 17 00:00:00 2001 From: Jason Tevnan Date: Fri, 2 Oct 2020 10:57:34 +0200 Subject: [PATCH 06/19] Jt/add sorting for foreignkey parents (#3) * issue #40 - should drop FKs before indexes and create FKs after indexes * Use parents to sort list of constraints * And please dont leak the password Co-authored-by: Ralph Bolton Co-authored-by: _/\__/\__/\__/\_ <671148+357r4bd@users.noreply.github.com> --- lib/MySQL/Diff.pm | 103 +++++++++++++++++++++++++++++++++++----- lib/MySQL/Diff/Table.pm | 10 ++++ 2 files changed, 102 insertions(+), 11 deletions(-) diff --git a/lib/MySQL/Diff.pm b/lib/MySQL/Diff.pm index da84922..0efcd0b 100644 --- a/lib/MySQL/Diff.pm +++ b/lib/MySQL/Diff.pm @@ -113,38 +113,89 @@ 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); + $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'}; + $diffs="DROP TABLE $name;\n\n"; + # 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'}; + $diffs = $table2->def() . "\n"; + # push @changes, $diffs + # unless $self->{opts}{'only-both'}; } + $unsorted_changes{$name}{'diffs'}=$diffs; + $unsorted_changes{$name}{'parents'}=$parents; + } + + # Sort for Parents + my %checked_changes; + debug(1,"Start sorting for parental constraints"); + debug(1,"Lets see: ".Dumper(%unsorted_changes)); + foreach my $t (keys %unsorted_changes) { + debug(1,"Checking table: ".$t); + push @changes, add($t); } - debug(4,join '', @changes); + sub add { + my $table = $_[0]; + + if (exists $checked_changes{$table}) { + debug(1,"table ".$table." in sorted hash, skipping"); + return; + }else{ + debug(1,"table ".$table." not in sorted hash, adding"); + } + + if (exists $unsorted_changes{$table}{'parents'}) { + debug(1, $table." has parents, checking"); + }else{ + debug(1, $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(1,"Doing parent table: ".$parent." of ".$table); + push @tmparray, add($parent); + } + $checked_changes{$table} = "done"; + push @tmparray, $unsorted_changes{$table}{'diffs'}; + return @tmparray; + } + debug(1,"Finished sorting for parental constraints"); + + + debug(1,join '', @changes); my $out = ''; if (@changes) { @@ -172,7 +223,7 @@ 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; @@ -190,12 +241,13 @@ EOF sub _diff_tables { my $self = shift; - my @changes = ( + my @changes = ( + $self->_diff_foreign_key_drop(@_), $self->_diff_fields(@_), $self->_diff_indices(@_), $self->_diff_partitions(@_), $self->_diff_primary_key(@_), - $self->_diff_foreign_key(@_), + $self->_diff_foreign_key_add(@_), $self->_diff_options(@_) ); @@ -425,7 +477,7 @@ sub _diff_primary_key { return @changes; } -sub _diff_foreign_key { +sub _diff_foreign_key_drop { my ($self, $table1, $table2) = @_; my $name1 = $table1->name(); @@ -448,7 +500,6 @@ sub _diff_foreign_key { my $changes = "ALTER TABLE $name1 DROP FOREIGN KEY $fk;"; $changes .= " # was CONSTRAINT $fk $fks1->{$fk}" unless $self->{opts}{'no-old-defs'}; - $changes .= "\nALTER TABLE $name1 ADD CONSTRAINT $fk FOREIGN KEY $fks2->{$fk};\n"; push @changes, $changes; } } else { @@ -462,6 +513,36 @@ sub _diff_foreign_key { } } + return @changes; +} + +sub _diff_foreign_key_add { + my ($self, $table1, $table2) = @_; + + my $name1 = $table1->name(); + + my $fks1 = $table1->foreign_key(); + my $fks2 = $table2->foreign_key(); + + 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}) + { + debug(1,"foreign key '$fk' changed"); + my $changes = "\nALTER TABLE $name1 ADD CONSTRAINT $fk FOREIGN KEY $fks2->{$fk};\n"; + push @changes, $changes; + } + } + } + } + if($fks2) { for my $fk (keys %$fks2) { next if($fks1 && $fks1->{$fk}); diff --git a/lib/MySQL/Diff/Table.pm b/lib/MySQL/Diff/Table.pm index 1f8ea35..3428169 100644 --- a/lib/MySQL/Diff/Table.pm +++ b/lib/MySQL/Diff/Table.pm @@ -15,6 +15,7 @@ MySQL::Diff::Table - Table Definition Class my $fields = $db->fields(); # %$fields my $primary_key = $db->primary_key(); my $indices = $db->indices(); # %$indices + my $parents = $db->parents(); # %$parents my $partitions = $db->partitions(); # %$partitions my $options = $db->options(); @@ -104,6 +105,10 @@ 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. @@ -155,6 +160,7 @@ 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}; } @@ -207,6 +213,10 @@ sub _parse { if (/^(?:CONSTRAINT\s+(.*)?)?\s+FOREIGN\s+KEY\s+(.*)$/) { my ($key, $val) = ($1, $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); + $self->{parents}{$const_parent_table} = $const_name; + } croak "foreign key '$key' duplicated in table '$name'\n" if $self->{foreign_key}{$key}; debug(1,"got foreign key $key"); From 4b7367bbe0671183395c4833ca498ad1d6edd664 Mon Sep 17 00:00:00 2001 From: Jason Tevnan Date: Mon, 5 Oct 2020 23:24:05 +0200 Subject: [PATCH 07/19] Fixed issues with wrong calles to arrays --- lib/MySQL/Diff.pm | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/lib/MySQL/Diff.pm b/lib/MySQL/Diff.pm index 0efcd0b..c322f03 100644 --- a/lib/MySQL/Diff.pm +++ b/lib/MySQL/Diff.pm @@ -119,7 +119,7 @@ sub diff { debug(1, "\ncomparing databases"); for my $table1 ($self->db1->tables()) { - my $diffs; + my @diffs; my $name = $table1->name(); my $parents = $table1->parents(); $used_tables{'-- '. $name} = 1; @@ -127,20 +127,21 @@ sub diff { debug(2,"looking at tables called '$name'"); if (my $table2 = $self->db2->table_by_name($name)) { debug(3,"comparing tables called '$name'"); - $diffs = $self->_diff_tables($table1, $table2); + push @diffs, $self->_diff_tables($table1, $table2); # push @changes, $diffs; } else { debug(3,"table '$name' dropped"); - $diffs="DROP TABLE $name;\n\n"; + 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}{'diffs'} = [@diffs]; $unsorted_changes{$name}{'parents'}=$parents; } for my $table2 ($self->db2->tables()) { - my $diffs; + my @diffs; my $name = $table2->name(); my $parents = $table2->parents(); $used_tables{'-- '. $name} = 1; @@ -148,20 +149,22 @@ sub diff { if (! $self->db1->table_by_name($name)) { debug(3,"table '$name' added"); debug(4,"table '$name' added '".$table2->def()."'"); - $diffs = $table2->def() . "\n"; + push @diffs, $table2->def() . "\n" + unless $self->{opts}{'only-both'}; # push @changes, $diffs # unless $self->{opts}{'only-both'}; } - $unsorted_changes{$name}{'diffs'}=$diffs; + push @{$unsorted_changes{$name}{'diffs'}},@diffs; $unsorted_changes{$name}{'parents'}=$parents; } + debug(1,"Unsorted_changes: ".Dumper(%unsorted_changes)); + # Sort for Parents my %checked_changes; debug(1,"Start sorting for parental constraints"); - debug(1,"Lets see: ".Dumper(%unsorted_changes)); foreach my $t (keys %unsorted_changes) { - debug(1,"Checking table: ".$t); + debug(2,"Checking table: ".$t); push @changes, add($t); } @@ -169,27 +172,28 @@ sub diff { my $table = $_[0]; if (exists $checked_changes{$table}) { - debug(1,"table ".$table." in sorted hash, skipping"); + debug(5,"table ".$table." in sorted hash, skipping"); return; }else{ - debug(1,"table ".$table." not in sorted hash, adding"); + debug(5,"table ".$table." not in sorted hash, adding"); } if (exists $unsorted_changes{$table}{'parents'}) { - debug(1, $table." has parents, checking"); + debug(5, $table." has parents, checking"); }else{ - debug(1, $table." has no parents, returning"); + debug(5, $table." has no parents, returning"); $checked_changes{$table} = "done"; - return $unsorted_changes{$table}{'diffs'}; + return @{$unsorted_changes{$table}{'diffs'}}; } my @tmparray; foreach my $parent (keys %{$unsorted_changes{$table}{'parents'}}) { - debug(1,"Doing parent table: ".$parent." of ".$table); + 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'}; + push @tmparray, @{$unsorted_changes{$table}{'diffs'}}; return @tmparray; } debug(1,"Finished sorting for parental constraints"); From a98092fe79ccf59f65c1c227934f22723c6c8275 Mon Sep 17 00:00:00 2001 From: Jason Tevnan Date: Tue, 9 Mar 2021 19:31:39 +0100 Subject: [PATCH 08/19] switch from name to concated values for fks (#4) * switch from name to concated values for fks * The names also refect the FKs --- lib/MySQL/Diff/Table.pm | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/lib/MySQL/Diff/Table.pm b/lib/MySQL/Diff/Table.pm index 3428169..08bc41a 100644 --- a/lib/MySQL/Diff/Table.pm +++ b/lib/MySQL/Diff/Table.pm @@ -212,25 +212,28 @@ sub _parse { } if (/^(?:CONSTRAINT\s+(.*)?)?\s+FOREIGN\s+KEY\s+(.*)$/) { - my ($key, $val) = ($1, $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); - $self->{parents}{$const_parent_table} = $const_name; + debug(1,"new foreign key $const_local_column-$const_parent_table-$const_parent_column"); + my $key = "${const_local_column}|${const_parent_table}|${const_parent_column}"; + my $val = "${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} = $val; + next; } - 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; } 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}; - $self->{indices}{$key} = $val; - $self->{unique}{$key} = 1 if($type =~ /unique/i); - debug(4, "got ", defined $self->{unique}{$key} ? 'unique ' : '', "index key '$key': ($val)"); + if $self->{indices}{$val}; + $self->{indices}{$val} = $val; + $self->{unique}{$val} = 1 if($type =~ /unique/i); + debug(4, "got ", defined $self->{unique}{$val} ? 'unique ' : '', "index key '$val': ($val)"); next; } From 65be25d59049edd2d00b5b4ebd026d2209ec7666 Mon Sep 17 00:00:00 2001 From: Jason Tevnan Date: Mon, 29 Mar 2021 19:23:56 +0200 Subject: [PATCH 09/19] Ignoring identical indexes on same columns ... not a reason to fail --- lib/MySQL/Diff/Table.pm | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/MySQL/Diff/Table.pm b/lib/MySQL/Diff/Table.pm index 08bc41a..08166ec 100644 --- a/lib/MySQL/Diff/Table.pm +++ b/lib/MySQL/Diff/Table.pm @@ -229,8 +229,6 @@ sub _parse { 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}{$val}; $self->{indices}{$val} = $val; $self->{unique}{$val} = 1 if($type =~ /unique/i); debug(4, "got ", defined $self->{unique}{$val} ? 'unique ' : '', "index key '$val': ($val)"); From cd1f1db5e6592c848aadbbc33339ae7abc87cf6a Mon Sep 17 00:00:00 2001 From: Jason Tevnan Date: Wed, 14 Apr 2021 15:53:15 +0200 Subject: [PATCH 10/19] Forgot about multi column indexes and names having columns suddenly... (#5) --- lib/MySQL/Diff/Table.pm | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/MySQL/Diff/Table.pm b/lib/MySQL/Diff/Table.pm index 08166ec..5b7fbf6 100644 --- a/lib/MySQL/Diff/Table.pm +++ b/lib/MySQL/Diff/Table.pm @@ -229,9 +229,11 @@ sub _parse { 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); - $self->{indices}{$val} = $val; - $self->{unique}{$val} = 1 if($type =~ /unique/i); - debug(4, "got ", defined $self->{unique}{$val} ? 'unique ' : '', "index key '$val': ($val)"); + 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; } From e86d4e52283d09dda0348abcd5ab9f3b65b231c1 Mon Sep 17 00:00:00 2001 From: Jason Tevnan Date: Tue, 18 May 2021 12:19:10 +0200 Subject: [PATCH 11/19] value of FKs obviously have to be valid sql. Also added namespacing to fk_name --- lib/MySQL/Diff/Table.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/MySQL/Diff/Table.pm b/lib/MySQL/Diff/Table.pm index 5b7fbf6..cd8b739 100644 --- a/lib/MySQL/Diff/Table.pm +++ b/lib/MySQL/Diff/Table.pm @@ -212,11 +212,11 @@ sub _parse { } if (/^(?:CONSTRAINT\s+(.*)?)?\s+FOREIGN\s+KEY\s+(.*)$/) { + 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 = "${const_local_column}|${const_parent_table}|${const_parent_column}"; - my $val = "${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" From d4a745472f9e13f0b19de5ed881fb183fbcc9970 Mon Sep 17 00:00:00 2001 From: Jason Tevnan Date: Mon, 9 Oct 2023 12:10:56 +0200 Subject: [PATCH 12/19] catch error finally --- lib/MySQL/Diff/Database.pm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/MySQL/Diff/Database.pm b/lib/MySQL/Diff/Database.pm index 41fa156..5f191db 100644 --- a/lib/MySQL/Diff/Database.pm +++ b/lib/MySQL/Diff/Database.pm @@ -220,7 +220,8 @@ sub _canonicalise_file { 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. From af54495e89b038a1dc0188cd6baeaac621514ed0 Mon Sep 17 00:00:00 2001 From: Jason Tevnan Date: Thu, 16 Nov 2023 12:07:08 +0100 Subject: [PATCH 13/19] seperate name from fk (#6) --- lib/MySQL/Diff.pm | 17 +++++++++-------- lib/MySQL/Diff/Table.pm | 3 ++- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/MySQL/Diff.pm b/lib/MySQL/Diff.pm index c322f03..b82e288 100644 --- a/lib/MySQL/Diff.pm +++ b/lib/MySQL/Diff.pm @@ -498,18 +498,18 @@ sub _diff_foreign_key_drop { 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; @@ -537,13 +537,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; } } + } } @@ -551,7 +552,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"; } } diff --git a/lib/MySQL/Diff/Table.pm b/lib/MySQL/Diff/Table.pm index cd8b739..7c1e91b 100644 --- a/lib/MySQL/Diff/Table.pm +++ b/lib/MySQL/Diff/Table.pm @@ -222,7 +222,8 @@ sub _parse { 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; + $self->{foreign_key}{$key}{value} = $val; + $self->{foreign_key}{$key}{name} = $const_name; next; } } From d570c4780f45f3ce58e68c6804debe23d845c741 Mon Sep 17 00:00:00 2001 From: Jason Tevnan Date: Mon, 22 Jan 2024 19:17:18 +0100 Subject: [PATCH 14/19] Fix toleration and colation (#7) --- lib/MySQL/Diff.pm | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/MySQL/Diff.pm b/lib/MySQL/Diff.pm index b82e288..50e2741 100644 --- a/lib/MySQL/Diff.pm +++ b/lib/MySQL/Diff.pm @@ -279,6 +279,7 @@ sub _diff_fields { if ($fields2 && $f2) { if ($self->{opts}{tolerant}) { for ($f1, $f2) { + s/ CHARACTER SET [\w_]+//gi; s/ COLLATE [\w_]+//gi; } } From 3527812a34ee750d7c3c923d621d2b9371704e2c Mon Sep 17 00:00:00 2001 From: Jason Tevnan Date: Mon, 22 Jan 2024 21:18:39 +0100 Subject: [PATCH 15/19] Jt/change how collation charsets are handled (#8) * adds new table level engine,charsets,collate * replaces row level collate/charset if matches table level --- lib/MySQL/Diff.pm | 22 ++++++++++++++++------ lib/MySQL/Diff/Table.pm | 26 ++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/lib/MySQL/Diff.pm b/lib/MySQL/Diff.pm index 50e2741..29a6ac4 100644 --- a/lib/MySQL/Diff.pm +++ b/lib/MySQL/Diff.pm @@ -267,6 +267,12 @@ 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; @@ -277,12 +283,15 @@ sub _diff_fields { my $f1 = $fields1->{$field}; my $f2 = $fields2->{$field}; if ($fields2 && $f2) { - if ($self->{opts}{tolerant}) { - for ($f1, $f2) { - s/ CHARACTER SET [\w_]+//gi; - 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 (($f1 !~ m/$f2\(\d+,\d+\)/) and @@ -615,6 +624,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; } diff --git a/lib/MySQL/Diff/Table.pm b/lib/MySQL/Diff/Table.pm index 7c1e91b..85a0e96 100644 --- a/lib/MySQL/Diff/Table.pm +++ b/lib/MySQL/Diff/Table.pm @@ -18,6 +18,9 @@ MySQL::Diff::Table - Table Definition Class my $parents = $db->parents(); # %$parents my $partitions = $db->partitions(); # %$partitions my $options = $db->options(); + my $engine = $db->engine(); + my $charset = $db->charset(); + my $collate = $db->collate(); my $isfield = $db->isa_field($field); my $isprimary = $db->isa_primary($field); @@ -117,6 +120,18 @@ Returns a hash reference to fields used as partitions. 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 @@ -164,6 +179,9 @@ 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; } @@ -261,6 +279,14 @@ sub _parse { if (/^\)\s*(.*?)(;?)$/) { # end of table definition $self->{options} = $1; + 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 ($2){ # there is a ; at the end debug(4,"got table options '$self->{options}'"); last; From 2647d94f36a3edeb94ec6b5d0e77cfcbcc4f07d7 Mon Sep 17 00:00:00 2001 From: Jason Tevnan Date: Tue, 23 Jan 2024 09:32:40 +0100 Subject: [PATCH 16/19] Ignore autoincrement addition --- lib/MySQL/Diff/Table.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/MySQL/Diff/Table.pm b/lib/MySQL/Diff/Table.pm index 85a0e96..7e508d0 100644 --- a/lib/MySQL/Diff/Table.pm +++ b/lib/MySQL/Diff/Table.pm @@ -279,7 +279,7 @@ sub _parse { if (/^\)\s*(.*?)(;?)$/) { # end of table definition $self->{options} = $1; - if (/^\)\s*ENGINE=([^\s;]+)\s+DEFAULT CHARSET=([^\s;]+)\s+COLLATE=([^\s;]+);/) { + if (/^\)\s*ENGINE=([^\s;]+).*\s+DEFAULT CHARSET=([^\s;]+)\s+COLLATE=([^\s;]+);/) { $self->{engine} = $1; $self->{charset} = $2; $self->{collate} = $3; From ec044f027e73f2b6d8d5ebf1b60a8e2c8a21db2b Mon Sep 17 00:00:00 2001 From: Jason Tevnan Date: Mon, 26 Feb 2024 09:00:20 +0100 Subject: [PATCH 17/19] support charset/collation for partitioned tables (#9) regexp stopped before partition :doh: --- lib/MySQL/Diff/Table.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/MySQL/Diff/Table.pm b/lib/MySQL/Diff/Table.pm index 7e508d0..a02e7d8 100644 --- a/lib/MySQL/Diff/Table.pm +++ b/lib/MySQL/Diff/Table.pm @@ -279,7 +279,7 @@ sub _parse { if (/^\)\s*(.*?)(;?)$/) { # end of table definition $self->{options} = $1; - if (/^\)\s*ENGINE=([^\s;]+).*\s+DEFAULT CHARSET=([^\s;]+)\s+COLLATE=([^\s;]+);/) { + if (/^\)\s*ENGINE=([^\s;]+).*\s+DEFAULT CHARSET=([^\s;]+)\s+COLLATE=([^\s;]+)/) { $self->{engine} = $1; $self->{charset} = $2; $self->{collate} = $3; From d912ea8e83fa7bc3955627be37a32e1696d9bf29 Mon Sep 17 00:00:00 2001 From: Jason Tevnan Date: Mon, 8 Apr 2024 12:15:16 +0200 Subject: [PATCH 18/19] fix ending recognition (#10) --- lib/MySQL/Diff/Table.pm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/MySQL/Diff/Table.pm b/lib/MySQL/Diff/Table.pm index a02e7d8..8fed461 100644 --- a/lib/MySQL/Diff/Table.pm +++ b/lib/MySQL/Diff/Table.pm @@ -279,6 +279,7 @@ sub _parse { if (/^\)\s*(.*?)(;?)$/) { # end of table definition $self->{options} = $1; + my $ending = $2; if (/^\)\s*ENGINE=([^\s;]+).*\s+DEFAULT CHARSET=([^\s;]+)\s+COLLATE=([^\s;]+)/) { $self->{engine} = $1; $self->{charset} = $2; @@ -287,7 +288,7 @@ sub _parse { } else { debug(1,"no regexp match for option content"); } - if ($2){ # there is a ; at the end + if ($ending){ # there is a ; at the end debug(4,"got table options '$self->{options}'"); last; } From 925ee7fba0cccf1d6bf2b043d0bbdf04d8c1c64b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oriol=20Franc=C3=A8s=20Acosta?= Date: Thu, 18 Apr 2024 09:31:59 +0200 Subject: [PATCH 19/19] Add event support (#11) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Oriol Francès --- bin/mysqldiff | 16 ++++- lib/MySQL/Diff.pm | 121 +++++++++++++++++++++++++++++++------ lib/MySQL/Diff/Database.pm | 107 +++++++++++++++++++++++++------- lib/MySQL/Diff/Event.pm | 114 ++++++++++++++++++++++++++++++++++ lib/MySQL/Diff/Table.pm | 50 +++++++-------- lib/MySQL/Diff/Utils.pm | 4 +- t/all.t | 95 +++++++++++++++++++++++++++-- 7 files changed, 432 insertions(+), 75 deletions(-) create mode 100644 lib/MySQL/Diff/Event.pm 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 29a6ac4..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; } @@ -118,6 +118,7 @@ sub diff { debug(1, "\ncomparing databases"); + for my $table1 ($self->db1->tables()) { my @diffs; my $name = $table1->name(); @@ -157,7 +158,7 @@ sub diff { push @{$unsorted_changes{$name}{'diffs'}},@diffs; $unsorted_changes{$name}{'parents'}=$parents; } - + debug(1,"Unsorted_changes: ".Dumper(%unsorted_changes)); # Sort for Parents @@ -170,14 +171,14 @@ sub diff { 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{ @@ -185,7 +186,7 @@ sub diff { $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); @@ -198,6 +199,30 @@ sub diff { } 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); @@ -234,7 +259,7 @@ sub _diff_banner { my $now = scalar localtime(); return <_diff_partitions(@_), $self->_diff_primary_key(@_), $self->_diff_foreign_key_add(@_), - $self->_diff_options(@_) + $self->_diff_options(@_) ); $changes[-1] =~ s/\n*$/\n/ if (@changes); @@ -276,7 +301,7 @@ sub _diff_fields { return () unless $fields1 || $fields2; my @changes; - + if($fields1) { for my $field (keys %$fields1) { debug(3,"table1 had field '$field'"); @@ -293,7 +318,7 @@ sub _diff_fields { 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") )) @@ -351,7 +376,7 @@ 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'; @@ -362,7 +387,7 @@ sub _diff_indices { ($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'; @@ -402,6 +427,68 @@ sub _diff_indices { 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) = @_; @@ -462,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); @@ -502,13 +589,13 @@ 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}->{'value'} ne $fks2->{$fk}->{'value'}) + if($fks1->{$fk}->{'value'} ne $fks2->{$fk}->{'value'}) { debug(1,"foreign key '$fk' changed"); my $changes = "ALTER TABLE $name1 DROP FOREIGN KEY $fks1->{$fk}->{'name'};"; @@ -661,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 5f191db..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,11 +237,11 @@ 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"; @@ -284,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'} ) { @@ -294,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; @@ -319,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|\/\*\!\d{5}\sSET)/, @{$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 8fed461..79c5748 100644 --- a/lib/MySQL/Diff/Table.pm +++ b/lib/MySQL/Diff/Table.pm @@ -8,27 +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 $parents = $db->parents(); # %$parents - my $partitions = $db->partitions(); # %$partitions - my $options = $db->options(); - my $engine = $db->engine(); - my $charset = $db->charset(); - my $collate = $db->collate(); - - 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 $isspatial = $db->is_spatial($field); - my $isfulltext = $db->is_fulltext($field); - my $ipatitioned = $db->is_paritioned($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 @@ -163,7 +163,7 @@ Returns 1 if given field is defined as an auto increment field, otherwise return =item * is_paritioned -Returns if given fiel is a praritioned field +Returns if given field is a paritioned field =back @@ -228,7 +228,7 @@ sub _parse { next; } - + if (/^(?:CONSTRAINT\s+(.*)?)?\s+FOREIGN\s+KEY\s+(.*)$/) { my $val = $2; if (/^(?:CONSTRAINT\s+(.*)?)?\s+FOREIGN\s+KEY\s+\((.+?)\)\sREFERENCES\s(.+?)\s\((.+?)\)(.*)/) { @@ -236,7 +236,7 @@ sub _parse { 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; + $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"); @@ -287,7 +287,7 @@ sub _parse { 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; 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 58f8a0b..ba5346a 100644 --- a/t/all.t +++ b/t/all.t @@ -250,7 +250,37 @@ CREATE TABLE piq ( 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 = ( @@ -328,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; @@ -460,7 +490,6 @@ ALTER TABLE foo ADD PRIMARY KEY (id,foreign_id); ALTER TABLE foo DROP INDEX id; ', ], - 'drop additional primary key' => [ {}, @@ -495,7 +524,6 @@ ALTER TABLE foo DROP INDEX id; ALTER TABLE bar ADD UNIQUE name (name,age); ', ], - 'drop index' => [ {}, @@ -511,7 +539,6 @@ ALTER TABLE bar ADD UNIQUE name (name,age); ALTER TABLE bar DROP INDEX name; # was UNIQUE (name,age) ', ], - 'alter indices' => [ {}, @@ -740,6 +767,62 @@ ALTER TABLE piq ADD PARTITION (PARTITION p7 VALUES IN (7)); 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();