mirror of
https://github.com/percona/percona-toolkit.git
synced 2025-09-10 13:11:32 +00:00
386 lines
11 KiB
Perl
386 lines
11 KiB
Perl
#!/usr/bin/env perl
|
|
|
|
BEGIN {
|
|
die "The PERCONA_TOOLKIT_BRANCH environment variable is not set.\n"
|
|
unless $ENV{PERCONA_TOOLKIT_BRANCH} && -d $ENV{PERCONA_TOOLKIT_BRANCH};
|
|
unshift @INC, "$ENV{PERCONA_TOOLKIT_BRANCH}/lib";
|
|
};
|
|
|
|
use strict;
|
|
use warnings FATAL => 'all';
|
|
use English qw(-no_match_vars);
|
|
use Test::More;
|
|
|
|
use PerconaTest;
|
|
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 $master_dbh = $sb->get_dbh_for('master');
|
|
my $slave_dbh = $sb->get_dbh_for('slave1');
|
|
|
|
if ( !$master_dbh ) {
|
|
plan skip_all => 'Cannot connect to sandbox master';
|
|
}
|
|
else {
|
|
plan tests => 38;
|
|
}
|
|
|
|
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;
|
|
|
|
# #############################################################################
|
|
# 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 { $exit = pt_online_schema_change::main(@args, "$dsn,t=pt_osc.t",
|
|
qw(--new-table mysql.user)) }
|
|
);
|
|
|
|
like(
|
|
$output,
|
|
qr/neither --dry-run nor --execute was specified/,
|
|
"Doesn't run without --execute (bug 933232)"
|
|
);
|
|
|
|
is(
|
|
$exit,
|
|
0,
|
|
"Exit 0"
|
|
);
|
|
|
|
# #############################################################################
|
|
# A helper sub to do the heavy lifting for us.
|
|
# #############################################################################
|
|
|
|
sub test_alter_table {
|
|
my (%args) = @_;
|
|
return if $args{skip};
|
|
|
|
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};
|
|
|
|
my ($db, $tbl) = $q->split_unquote($table);
|
|
my $pk_col = $args{pk_col} || 'id';
|
|
|
|
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`");
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
# #############################################################################
|
|
# The most basic: alter a small table with no fks that's not active.
|
|
# #############################################################################
|
|
|
|
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)],
|
|
);
|
|
|
|
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)],
|
|
);
|
|
|
|
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)],
|
|
);
|
|
|
|
# ############################################################################
|
|
# Alter a table with foreign keys.
|
|
# ############################################################################
|
|
|
|
# The tables we're loading have fk constraints like:
|
|
# country <-- city <-- address
|
|
|
|
# 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.
|
|
|
|
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',
|
|
],
|
|
);
|
|
|
|
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',
|
|
],
|
|
);
|
|
|
|
# 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',
|
|
],
|
|
);
|
|
|
|
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',
|
|
],
|
|
);
|
|
|
|
# #############################################################################
|
|
# Alter tables with columns with resvered words and spaces.
|
|
# #############################################################################
|
|
sub test_table {
|
|
my (%args) = @_;
|
|
my ($file, $name) = @args{qw(file name)};
|
|
|
|
$sb->load_file('master', "t/lib/samples/osc/$file");
|
|
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");
|
|
|
|
my $org_rows = $master_dbh->selectall_arrayref('select * from osc.t order by id');
|
|
|
|
output(
|
|
sub { $exit = pt_online_schema_change::main(@args,
|
|
"$dsn,D=osc,t=t", qw(--alter ENGINE=InnoDB)) },
|
|
);
|
|
|
|
my $new_rows = $master_dbh->selectall_arrayref('select * from osc.t order by id');
|
|
|
|
is_deeply(
|
|
$new_rows,
|
|
$org_rows,
|
|
"$name rows"
|
|
);
|
|
|
|
is(
|
|
$exit,
|
|
0,
|
|
"$name exit status 0"
|
|
);
|
|
}
|
|
|
|
test_table(
|
|
file => "tbl002.sql",
|
|
name => "Reserved word column",
|
|
);
|
|
|
|
test_table(
|
|
file => "tbl003.sql",
|
|
name => "Space column",
|
|
);
|
|
|
|
# #############################################################################
|
|
# Done.
|
|
# #############################################################################
|
|
$sb->wipe_clean($master_dbh);
|
|
exit;
|