Do not ever create --new-table. Rewrite basics.t. Add more debug statements.

This commit is contained in:
Daniel Nichter
2012-03-24 12:47:08 -06:00
parent 7add482367
commit d3ad8c84c0
4 changed files with 344 additions and 208 deletions

View File

@@ -5208,15 +5208,15 @@ sub main {
print "Starting a dry run. $orig_tbl->{name} will not be altered. "
. "Specify --execute instead of --dry-run to alter the table.\n";
}
elsif ( !$o->get('execute') ) {
elsif ( $o->get('execute') ) {
print "Altering $orig_tbl->{name}...\n";
}
else {
print "Exiting without altering $orig_tbl->{name} because neither "
. "--dry-run nor --execute was specified. Please read the tool's "
. "documentation carefully before using this tool.\n";
return $exit_status;
}
else {
print "Altering $orig_tbl->{name}...\n";
}
# ########################################################################
# Check and create PID file if user specified --pid.
@@ -5241,7 +5241,15 @@ sub main {
db => $db,
tbl => $tbl,
);
if ( !$new_table_exists ) {
if ( $new_table_exists ) {
# Do not create the new table if it already exists in case the user
# does something bad like --new-table wrong.table because then we'll
# modify the wrong table and really break stuff.
die "The --new-table $new_table already exists. Please specify "
. "a new table name that does not yet exist.\n";
}
else {
# --new-table does not exist. Create it?
if ( $o->get('create-new-table') ) {
my $sql = "CREATE TABLE $new_table LIKE $orig_tbl->{name}";
PTDEBUG && _d($sql);
@@ -5711,6 +5719,7 @@ sub main {
. "by executing: $drop_new_table_sql\n";
}
}
PTDEBUG && _d('Old table:', Dumper($old_tbl));
# #####################################################################
# Update foreign key constraints if there are child tables.
@@ -5983,7 +5992,9 @@ sub find_child_tables {
die "I need a $arg argument" unless $args{$arg};
}
my ($tbl, $cxn, $q) = @args{@required_args};
PTDEBUG && _d('Finding child tables');
my $sql = "SELECT table_name "
. "FROM information_schema.key_column_usage "
. "WHERE constraint_schema='$tbl->{db}' "
@@ -5995,8 +6006,8 @@ sub find_child_tables {
return;
}
PTDEBUG && _d("Child tables:", Dumper($child_tables));
my @child_tables = map { $_->[0] } @$child_tables;
PTDEBUG && _d("Child tables:", @child_tables);
return \@child_tables;
}
@@ -6013,6 +6024,8 @@ sub rebuild_constraints {
my $exit_status = 0;
my $constraint = qr/^\s+(CONSTRAINT.+?REFERENCES `$old_tbl->{tbl}`.+)$/m;
PTDEBUG && _d('Rebuilding fk constraint:', $constraint);
if ( $o->get('dry-run') ) {
print "Not rebuilding foreign key constraints because this is a dry run.\n";
}
@@ -6043,19 +6056,26 @@ sub rebuild_constraints {
}
foreach my $constraint ( @constraints ) {
my ($fk_symbol) = $constraint =~ m/CONSTRAINT\s+(\S+)/;
PTDEBUG && _d('Rewriting fk constraint:', $constraint);
my ($fk) = $constraint =~ m/CONSTRAINT\s+(\S+)/;
# Drop the reference to the old table/renamed orig table.
$sql = "ALTER TABLE $child_table DROP FOREIGN KEY $fk_symbol";
print $sql, "\n" if $o->get('print');
$cxn->dbh()->do($sql) if $o->get('execute');
$sql = "ALTER TABLE $child_table DROP FOREIGN KEY $fk";
print $sql, "\n" if $o->get('print');
if ( $o->get('execute') ) {
PTDEBUG && _d($sql);
$cxn->dbh()->do($sql);
}
# Make the child reference the new table, i.e. the table name it
# originally referenced instead of the old/renamed orig table.
$constraint =~ s/REFERENCES `$old_tbl->{tbl}`/REFERENCES `$orig_tbl->{tbl}`/o;
$constraint =~ s/REFERENCES `$old_tbl->{tbl}`/REFERENCES `$orig_tbl->{tbl}`/;
$sql = "ALTER TABLE $child_table ADD $constraint";
print $sql, "\n" if $o->get('print');
$cxn->dbh()->do($sql) if $o->get('execute');
print $sql, "\n" if $o->get('print');
if ( $o->get('execute') ) {
PTDEBUG && _d($sql);
$cxn->dbh()->do($sql);
}
}
}

View File

@@ -16,150 +16,227 @@ use Sandbox;
require "$trunk/bin/pt-online-schema-change";
use Data::Dumper;
$Data::Dumper::Indent = 1;
$Data::Dumper::Sortkeys = 1;
$Data::Dumper::Quotekeys = 0;
my $dp = new DSNParser(opts=>$dsn_opts);
my $sb = new Sandbox(basedir => '/tmp', DSNParser => $dp);
my $dbh = $sb->get_dbh_for('master');
my $dp = new DSNParser(opts=>$dsn_opts);
my $sb = new Sandbox(basedir => '/tmp', DSNParser => $dp);
my $master_dbh = $sb->get_dbh_for('master');
my $slave_dbh = $sb->get_dbh_for('slave1');
if ( !$dbh ) {
if ( !$master_dbh ) {
plan skip_all => 'Cannot connect to sandbox master';
}
else {
plan tests => 23;
plan tests => 38;
}
my $output = "";
my $cnf = '/tmp/12345/my.sandbox.cnf';
my @args = ('-F', $cnf, '--execute');
my $exit = 0;
my $q = new Quoter();
my $tp = new TableParser(Quoter => $q);
my @args = qw(--lock-wait-timeout 3);
my $output = "";
my $dsn = "h=127.1,P=12345,u=msandbox,p=msandbox";
my $exit = 0;
my $sample = "t/pt-online-schema-change/samples";
my $rows;
$sb->load_file('master', "t/pt-online-schema-change/samples/small_table.sql");
$dbh->do('use mkosc');
# #############################################################################
# Tool shouldn't run without --execute (bug 933232).
# #############################################################################
$sb->load_file('master', "$sample/basic_no_fks.sql");
PerconaTest::wait_for_table($slave_dbh, "pt_osc.t", "id=20");
# --new-table really ensures the tool exists before doing stuff because
# setting up the new table is the first thing the tool does and this would
# cause an error because mysql.user already exists. To prove this, add
# --dry-run and the test will fail because the code doesn't exit early.
$output = output(
sub { pt_online_schema_change::main('h=127.1,P=12345,u=msandbox,p=msandbox,D=mkosc,t=a') },
sub { $exit = pt_online_schema_change::main(@args, "$dsn,t=pt_osc.t",
qw(--new-table mysql.user)) }
);
like(
$output,
qr/you did not specify --execute/,
"Doesn't run without --execute"
);
# #############################################################################
# --check-tables-and-exit
# #############################################################################
eval {
$exit = pt_online_schema_change::main(@args,
'D=mkosc,t=a', qw(--check-tables-and-exit --quiet))
};
is(
$EVAL_ERROR,
"",
"--check-tables-and-exit"
qr/neither --dry-run nor --execute was specified/,
"Doesn't run without --execute (bug 933232)"
);
is(
$exit,
0,
"Exit status 0"
"Exit 0"
);
# #############################################################################
# --cleanup-and-exit
# #############################################################################
eval {
$exit = pt_online_schema_change::main(@args,
'D=mkosc,t=a', qw(--cleanup-and-exit --quiet))
};
is(
$EVAL_ERROR,
"",
"--cleanup-and-exit",
);
is(
$exit,
0,
"Exit status 0"
);
# #############################################################################
# The most basic: copy, alter and rename a small table that's not even active.
# A helper sub to do the heavy lifting for us.
# #############################################################################
output(
sub { $exit = pt_online_schema_change::main(@args,
'D=mkosc,t=a', qw(--alter ENGINE=InnoDB)) },
);
sub test_alter_table {
my (%args) = @_;
return if $args{skip};
$rows = $dbh->selectall_hashref('show table status from mkosc', 'name');
is(
$rows->{a}->{engine},
'InnoDB',
"New table ENGINE=InnoDB"
);
my @required_args = qw(name table test_type cmds);
foreach my $arg ( @required_args ) {
die "I need a $arg argument" unless $args{$arg};
}
my ($name, $table, $test_type, $cmds) = @args{@required_args};
is(
$rows->{__old_a}->{engine},
'MyISAM',
"Kept old table, ENGINE=MyISAM"
);
my ($db, $tbl) = $q->split_unquote($table);
my $pk_col = $args{pk_col} || 'id';
my $org_rows = $dbh->selectall_arrayref('select * from mkosc.__old_a order by i');
my $new_rows = $dbh->selectall_arrayref('select * from mkosc.a order by i');
is_deeply(
$new_rows,
$org_rows,
"New tables rows identical to old table rows"
);
if ( my $file = $args{file} ) {
$sb->load_file('master', "$sample/$file");
if ( my $wait = $args{wait} ) {
PerconaTest::wait_for_table($slave_dbh, @$wait);
}
else {
PerconaTest::wait_for_table($slave_dbh, $table, "`$pk_col`=$args{max_id}");
}
$master_dbh->do("USE `$db`");
$slave_dbh->do("USE `$db`");
}
is(
$exit,
0,
"Exit status 0"
);
my $ddl = $tp->get_create_table($master_dbh, $db, $tbl);
my $tbl_struct = $tp->parse($ddl);
my $cols = '*';
if ( $test_type eq 'drop_col' && !grep { $_ eq '--dry-run' } @$cmds ) {
# Don't select the column being dropped.
my $col = $args{drop_col};
die "I need a drop_col argument" unless $col;
$cols = join(', ', grep { $_ ne $col } @{$tbl_struct->{cols}});
}
my $orig_rows = $master_dbh->selectall_arrayref(
"SELECT $cols FROM $table ORDER BY `$pk_col`");
my $orig_tbls = $master_dbh->selectall_arrayref(
"SHOW TABLES FROM `$db`");
my @orig_fks;
if ( $args{check_fks} ) {
foreach my $tbl ( @$orig_tbls ) {
my $fks = $tp->get_fks(
$tp->get_create_table($master_dbh, $db, $tbl->[0]));
push @orig_fks, $fks;
}
}
$output = output(
sub { $exit = pt_online_schema_change::main(
@args,
"$dsn,D=$db,t=$tbl",
@$cmds,
)},
);
is(
$exit,
0,
"$name exit 0"
);
# There should be no new or missing tables.
my $new_tbls = $master_dbh->selectall_arrayref("SHOW TABLES FROM `$db`");
is_deeply(
$new_tbls,
$orig_tbls,
"$name tables"
);
# Rows in the original and new table should be identical.
my $new_rows = $master_dbh->selectall_arrayref("SELECT * FROM $table ORDER BY `$pk_col`");
is_deeply(
$new_rows,
$orig_rows,
"$name rows"
);
# Check if the ALTER was actually done.
if ( $test_type eq 'drop_col' ) {
my $col = $q->quote($args{drop_col});
my $ddl = $tp->get_create_table($master_dbh, $db, $tbl);
if ( grep { $_ eq '--dry-run' } @$cmds ) {
like(
$ddl,
qr/^\s+$col\s+/m,
"$name ALTER DROP COLUMN=$args{drop_col} (dry run)"
);
}
else {
unlike(
$ddl,
qr/^\s+$col\s+/m,
"$name ALTER DROP COLUMN=$args{drop_col}"
);
}
}
elsif ( $test_type eq 'add_col' ) {
}
elsif ( $test_type eq 'new_engine' ) {
my $new_engine = lc($args{new_engine});
die "I need a new_engine argument" unless $new_engine;
my $rows = $master_dbh->selectall_hashref(
"SHOW TABLE STATUS FROM `$db`", "name");
is(
lc($rows->{$tbl}->{engine}),
$new_engine,
"$name ALTER ENGINE=$args{new_engine}"
);
}
if ( $args{check_fks} ) {
my @new_fks;
foreach my $tbl ( @$orig_tbls ) {
my $fks = $tp->get_fks(
$tp->get_create_table($master_dbh, $db, $tbl->[0]));
push @new_fks, $fks;
}
is_deeply(
\@new_fks,
\@orig_fks,
"$name FK constraints"
);
}
return;
}
# #############################################################################
# No --alter and --drop-old-table.
# The most basic: alter a small table with no fks that's not active.
# #############################################################################
$dbh->do('drop table if exists mkosc.__old_a'); # from previous run
$sb->load_file('master', "t/pt-online-schema-change/samples/small_table.sql");
output(
sub { $exit = pt_online_schema_change::main(@args,
'D=mkosc,t=a', qw(--drop-old-table)) },
test_alter_table(
name => "Basic no fks --dry-run",
table => "pt_osc.t",
file => "basic_no_fks.sql",
max_id => 20,
test_type => "new_engine",
new_engine => "MyISAM",
cmds => [qw(--dry-run --alter ENGINE=InnoDB)],
);
$rows = $dbh->selectall_hashref('show table status from mkosc', 'name');
is(
$rows->{a}->{engine},
'MyISAM',
"No --alter, new table still ENGINE=MyISAM"
test_alter_table(
name => "Basic no fks --execute",
table => "pt_osc.t",
# The previous test should not have modified the table.
# file => "basic_no_fks.sql",
# max_id => 20,
test_type => "new_engine",
new_engine => "InnoDB",
cmds => [qw(--execute --alter ENGINE=InnoDB)],
);
ok(
!exists $rows->{__old_a},
"--drop-old-table"
);
$new_rows = $dbh->selectall_arrayref('select * from mkosc.a order by i');
is_deeply(
$new_rows,
$org_rows, # from previous run since old table was dropped this run
"New tables rows identical to old table rows"
);
is(
$exit,
0,
"Exit status 0"
test_alter_table(
name => "--execute but no --alter",
table => "pt_osc.t",
file => "basic_no_fks.sql",
max_id => 20,
test_type => "new_engine",
new_engine => "MyISAM",
cmds => [qw(--execute)],
);
# ############################################################################
@@ -167,82 +244,93 @@ is(
# ############################################################################
# The tables we're loading have fk constraints like:
# country
# ^- city (on update cascade)
# ^- address (on update cascade)
# country <-- city <-- address
############################
# rebuild_constraints method
############################
$sb->load_file('master', "t/pt-online-schema-change/samples/fk_tables_schema.sql");
# rebuild_constraints method -- This parses the fk constraint ddls from
# the create table ddl, rewrites them, then does an alter table on the
# child tables so they point back to the original table name.
# city has a fk constraint on country, so get its original table def.
my $orig_table_def = $dbh->selectrow_hashref('show create table mkosc.city')->{'create table'};
# Alter the parent table. The error we need to avoid is:
# DBD::mysql::db do failed: Cannot delete or update a parent row:
# a foreign key constraint fails [for Statement "DROP TABLE
# `mkosc`.`__old_country`"]
output(
sub { $exit = pt_online_schema_change::main(@args,
'D=mkosc,t=country', qw(--child-tables auto_detect --drop-old-table),
qw(--update-foreign-keys-method rebuild_constraints)) },
);
is(
$exit,
0,
"Exit status 0 (rebuild_constraints method)"
test_alter_table(
name => "Basic FK rebuild --dry-run",
table => "pt_osc.country",
pk_col => "country_id",
file => "basic_with_fks.sql",
wait => ["pt_osc.address", "address_id=5"],
test_type => "drop_col",
drop_col => "last_update",
check_fks => 1,
cmds => [
qw(
--dry-run
--find-child-tables
--update-foreign-keys-method rebuild_constraints
),
'--alter', 'DROP COLUMN last_update',
],
);
$rows = $dbh->selectall_arrayref('show tables from mkosc like "\_\_%"');
is_deeply(
$rows,
[],
"Old table dropped (rebuild_constraints method)"
test_alter_table(
name => "Basic FK rebuild --execute",
table => "pt_osc.country",
pk_col => "country_id",
# The previous test should not have modified the table.
# file => "basic_with_fks.sql",
# wait => ["pt_osc.address", "address_id=5"],
test_type => "drop_col",
drop_col => "last_update",
check_fks => 1,
cmds => [
qw(
--execute
--find-child-tables
--update-foreign-keys-method rebuild_constraints
),
'--alter', 'DROP COLUMN last_update',
],
);
# Get city's table def again and verify that its fk constraint still
# references country. When country was renamed to __old_country, MySQL
# also updated city's fk constraint to __old_country. We should have
# dropped and re-added that constraint exactly, changing only __old_country
# to country, like it originally was.
my $new_table_def = $dbh->selectrow_hashref('show create table mkosc.city')->{'create table'};
is(
$new_table_def,
$orig_table_def,
"Updated child table foreign key constraint (rebuild_constraints method)"
# drop_old_table method -- This method tricks MySQL by disabling fk checks,
# then dropping the original table and renaming the new table in its place.
# Since fk checks were disabled, MySQL doesn't update the child table fk refs.
# Somewhat dangerous, but quick. Downside: table doesn't exist for a moment.
test_alter_table(
name => "Basic FK drop-swap --dry-run",
table => "pt_osc.country",
pk_col => "country_id",
file => "basic_with_fks.sql",
wait => ["pt_osc.address", "address_id=5"],
test_type => "drop_col",
drop_col => "last_update",
check_fks => 1,
cmds => [
qw(
--dry-run
--find-child-tables
--update-foreign-keys-method drop_old_table
),
'--alter', 'DROP COLUMN last_update',
],
);
#######################
# drop_old_table method
#######################
$sb->load_file('master', "t/pt-online-schema-change/samples/fk_tables_schema.sql");
$orig_table_def = $dbh->selectrow_hashref('show create table mkosc.city')->{'create table'};
output(
sub { $exit = pt_online_schema_change::main(@args,
'D=mkosc,t=country', qw(--child-tables auto_detect),
qw(--update-foreign-keys-method drop_old_table)) },
);
is(
$exit,
0,
"Exit status 0 (drop_old_table method)"
);
$rows = $dbh->selectall_arrayref('show tables from mkosc like "\_\_%"');
is_deeply(
$rows,
[],
"Old table dropped (drop_old_table method)"
) or print Dumper($rows);
$new_table_def = $dbh->selectrow_hashref('show create table mkosc.city')->{'create table'};
is(
$new_table_def,
$orig_table_def,
"Updated child table foreign key constraint (drop_old_table method)"
test_alter_table(
name => "Basic FK drop-swap --execute",
table => "pt_osc.country",
pk_col => "country_id",
# The previous test should not have modified the table.
# file => "basic_with_fks.sql",
# wait => ["pt_osc.address", "address_id=5"],
test_type => "drop_col",
drop_col => "last_update",
check_fks => 1,
cmds => [
qw(
--execute
--find-child-tables
--update-foreign-keys-method drop_old_table
),
'--alter', 'DROP COLUMN last_update',
],
);
# #############################################################################
@@ -253,19 +341,19 @@ sub test_table {
my ($file, $name) = @args{qw(file name)};
$sb->load_file('master', "t/lib/samples/osc/$file");
PerconaTest::wait_for_table($dbh, "osc.t", "id=5");
PerconaTest::wait_for_table($dbh, "osc.__new_t");
$dbh->do('use osc');
$dbh->do("DROP TABLE IF EXISTS osc.__new_t");
PerconaTest::wait_for_table($master_dbh, "osc.t", "id=5");
PerconaTest::wait_for_table($master_dbh, "osc.__new_t");
$master_dbh->do('use osc');
$master_dbh->do("DROP TABLE IF EXISTS osc.__new_t");
$org_rows = $dbh->selectall_arrayref('select * from osc.t order by id');
my $org_rows = $master_dbh->selectall_arrayref('select * from osc.t order by id');
output(
sub { $exit = pt_online_schema_change::main(@args,
'D=osc,t=t', qw(--alter ENGINE=InnoDB)) },
"$dsn,D=osc,t=t", qw(--alter ENGINE=InnoDB)) },
);
$new_rows = $dbh->selectall_arrayref('select * from osc.t order by id');
my $new_rows = $master_dbh->selectall_arrayref('select * from osc.t order by id');
is_deeply(
$new_rows,
@@ -293,5 +381,5 @@ test_table(
# #############################################################################
# Done.
# #############################################################################
$sb->wipe_clean($dbh);
$sb->wipe_clean($master_dbh);
exit;

View File

@@ -1,12 +1,13 @@
DROP DATABASE IF EXISTS `mkosc`;
CREATE DATABASE `mkosc`;
USE `mkosc`;
CREATE TABLE a (
i int auto_increment primary key,
c char(16),
d date
DROP DATABASE IF EXISTS pt_osc;
CREATE DATABASE pt_osc;
USE pt_osc;
CREATE TABLE t (
id int auto_increment primary key,
c char(16),
d date,
unique index (c(16))
) ENGINE=MyISAM;
INSERT INTO a VALUES
INSERT INTO pt_osc.t VALUES
(null, 'a', now()),
(null, 'b', now()),
(null, 'c', now()),
@@ -16,7 +17,7 @@ INSERT INTO a VALUES
(null, 'g', now()),
(null, 'h', now()),
(null, 'i', now()),
(null, 'j', now()),
(null, 'j', now()), -- 10
(null, 'k', now()),
(null, 'l', now()),
(null, 'm', now()),
@@ -24,4 +25,6 @@ INSERT INTO a VALUES
(null, 'o', now()),
(null, 'p', now()),
(null, 'q', now()),
(null, 'r', now());
(null, 'r', now()),
(null, 's', now()),
(null, 't', now()); -- 20

View File

@@ -1,6 +1,8 @@
DROP DATABASE IF EXISTS `mkosc`;
CREATE DATABASE `mkosc`;
USE `mkosc`;
DROP DATABASE IF EXISTS pt_osc;
CREATE DATABASE pt_osc;
USE pt_osc;
SET foreign_key_checks=0;
CREATE TABLE `country` (
`country_id` smallint(5) unsigned NOT NULL AUTO_INCREMENT,
@@ -29,3 +31,26 @@ CREATE TABLE `address` (
KEY `idx_fk_city_id` (`city_id`),
CONSTRAINT `fk_address_city` FOREIGN KEY (`city_id`) REFERENCES `city` (`city_id`) ON UPDATE CASCADE
) ENGINE=InnoDB;
INSERT INTO pt_osc.country VALUES
(1, 'Canada', null),
(2, 'USA', null),
(3, 'Mexico', null),
(4, 'France', null),
(5, 'Spain', null);
INSERT INTO pt_osc.city VALUES
(null, 'Montréal', 1, null),
(null, 'New York', 2, null),
(null, 'Durango', 3, null),
(null, 'Paris', 4, null),
(null, 'Madrid', 5, null);
INSERT INTO pt_osc.address VALUES
(null, 'addy 1', 1, '10000', null),
(null, 'addy 2', 2, '20000', null),
(null, 'addy 3', 3, '30000', null),
(null, 'addy 4', 4, '40000', null),
(null, 'addy 5', 5, '50000', null);
SET foreign_key_checks=1;