#!/usr/bin/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 ChangeHandler; use Quoter; use DSNParser; use Sandbox; use PerconaTest; my $dp = new DSNParser(opts => $dsn_opts); my $sb = new Sandbox(basedir => '/tmp', DSNParser => $dp); my $master_dbh = $sb->get_dbh_for('master'); my $slave1_dbh = $sb->get_dbh_for('slave1'); throws_ok( sub { new ChangeHandler() }, qr/I need a Quoter/, 'Needs a Quoter', ); my @rows; my @dbhs; my $q = new Quoter(); my $ch = new ChangeHandler( Quoter => $q, right_db => 'test', # dst right_tbl => 'foo', left_db => 'test', # src left_tbl => 'test1', actions => [ sub { push @rows, $_[0]; push @dbhs, $_[1]; } ], replace => 0, queue => 0, ); $ch->change('INSERT', { a => 1, b => 2 }, [qw(a)] ); is_deeply(\@rows, ["INSERT INTO `test`.`foo`(`a`, `b`) VALUES ('1', '2')",], 'First row', ); $ch->change(undef, { a => 1, b => 2 }, [qw(a)] ); is_deeply( \@rows, ["INSERT INTO `test`.`foo`(`a`, `b`) VALUES ('1', '2')",], 'Skips undef action' ); is_deeply(\@rows, ["INSERT INTO `test`.`foo`(`a`, `b`) VALUES ('1', '2')",], 'First row', ); $ch->{queue} = 1; $ch->change('DELETE', { a => 1, b => 2 }, [qw(a)] ); is_deeply(\@rows, ["INSERT INTO `test`.`foo`(`a`, `b`) VALUES ('1', '2')",], 'Second row not there yet', ); $ch->process_rows(1); is_deeply(\@rows, [ "INSERT INTO `test`.`foo`(`a`, `b`) VALUES ('1', '2')", "DELETE FROM `test`.`foo` WHERE `a`='1' LIMIT 1", ], 'Second row there', ); $ch->{queue} = 2; $ch->change('UPDATE', { a => 1, b => 2 }, [qw(a)] ); $ch->process_rows(1); is_deeply(\@rows, [ "INSERT INTO `test`.`foo`(`a`, `b`) VALUES ('1', '2')", "DELETE FROM `test`.`foo` WHERE `a`='1' LIMIT 1", ], 'Third row not there', ); $ch->process_rows(); is_deeply(\@rows, [ "INSERT INTO `test`.`foo`(`a`, `b`) VALUES ('1', '2')", "DELETE FROM `test`.`foo` WHERE `a`='1' LIMIT 1", "UPDATE `test`.`foo` SET `b`='2' WHERE `a`='1' LIMIT 1", ], 'All rows', ); is_deeply( { $ch->get_changes() }, { REPLACE => 0, DELETE => 1, INSERT => 1, UPDATE => 1 }, 'Changes were recorded', ); # ############################################################################# # Test that the optional dbh is passed through to our actions. # ############################################################################# @rows = (); @dbhs = (); $ch->{queue} = 0; # 42 is a placeholder for the dbh arg. $ch->change('INSERT', { a => 1, b => 2 }, [qw(a)], 42); is_deeply( \@dbhs, [42], 'dbh passed through change()' ); $ch->{queue} = 1; @rows = (); @dbhs = (); $ch->change('INSERT', { a => 1, b => 2 }, [qw(a)], 42); is_deeply( \@dbhs, [], 'No dbh yet' ); $ch->process_rows(1); is_deeply( \@dbhs, [42], 'dbh passed through process_rows()' ); # ############################################################################# # Test switching direction (swap src/dst). # ############################################################################# $ch = new ChangeHandler( Quoter => $q, left_db => 'test', left_tbl => 'left_foo', right_db => 'test', right_tbl => 'right_foo', actions => [ sub { push @rows, $_[0]; push @dbhs, $_[1]; } ], replace => 0, queue => 0, ); @rows = (); @dbhs = (); # Default is left=source. $ch->set_src('right'); is( $ch->src, '`test`.`right_foo`', 'Changed src', ); is( $ch->dst, '`test`.`left_foo`', 'Changed dst' ); $ch->change('INSERT', { a => 1, b => 2 }, [qw(a)] ); is_deeply( \@rows, ["INSERT INTO `test`.`left_foo`(`a`, `b`) VALUES ('1', '2')",], 'INSERT new dst', ); $ch->change('DELETE', { a => 1, b => 2 }, [qw(a)] ); $ch->process_rows(1); is_deeply(\@rows, [ "INSERT INTO `test`.`left_foo`(`a`, `b`) VALUES ('1', '2')", "DELETE FROM `test`.`left_foo` WHERE `a`='1' LIMIT 1", ], 'DELETE new dst', ); # ############################################################################# # Test fetch_back(). # ############################################################################# SKIP: { skip 'Cannot connect to sandbox master', 1 unless $master_dbh; $master_dbh->do('DROP DATABASE IF EXISTS test'); $master_dbh->do('CREATE DATABASE IF NOT EXISTS test'); $ch = new ChangeHandler( Quoter => $q, right_db => 'test', # dst right_tbl => 'foo', left_db => 'test', # src left_tbl => 'test1', actions => [ sub { push @rows, $_[0]; push @dbhs, $_[1]; } ], replace => 0, queue => 0, ); @rows = (); $ch->{queue} = 0; $ch->fetch_back($master_dbh); `/tmp/12345/use < $trunk/t/lib/samples/before-TableSyncChunk.sql`; # This should cause it to fetch the row from test.test1 where a=1 $ch->change('UPDATE', { a => 1, __foo => 'bar' }, [qw(a)] ); $ch->change('DELETE', { a => 1, __foo => 'bar' }, [qw(a)] ); # TODO: Sometimes this is retrieved as (b, a) instead of (a, b) #$ch->change('INSERT', { a => 1, __foo => 'bar' }, [qw(a)] ); is_deeply( \@rows, [ "UPDATE `test`.`foo` SET `b`='en' WHERE `a`='1' LIMIT 1", "DELETE FROM `test`.`foo` WHERE `a`='1' LIMIT 1", # "INSERT INTO `test`.`foo`(`a`, `b`) VALUES ('1', 'en')", ], 'Fetch-back', ); } # ############################################################################# # Issue 371: Make mk-table-sync preserve column order in SQL # ############################################################################# my $row = { id => 1, foo => 'foo', bar => 'bar', }; my $tbl_struct = { col_posn => { id=>0, foo=>1, bar=>2 }, }; $ch = new ChangeHandler( Quoter => $q, right_db => 'test', # dst right_tbl => 'issue_371', left_db => 'test', # src left_tbl => 'issue_371', actions => [ sub { push @rows, @_ } ], replace => 0, queue => 0, tbl_struct => $tbl_struct, ); @rows = (); @dbhs = (); is( $ch->make_INSERT($row, [qw(id foo bar)]), "INSERT INTO `test`.`issue_371`(`id`, `foo`, `bar`) VALUES ('1', 'foo', 'bar')", 'make_INSERT() preserves column order' ); is( $ch->make_REPLACE($row, [qw(id foo bar)]), "REPLACE INTO `test`.`issue_371`(`id`, `foo`, `bar`) VALUES ('1', 'foo', 'bar')", 'make_REPLACE() preserves column order' ); is( $ch->make_UPDATE($row, [qw(id foo)]), "UPDATE `test`.`issue_371` SET `bar`='bar' WHERE `id`='1' AND `foo`='foo' LIMIT 1", 'make_UPDATE() preserves column order' ); is( $ch->make_DELETE($row, [qw(id foo bar)]), "DELETE FROM `test`.`issue_371` WHERE `id`='1' AND `foo`='foo' AND `bar`='bar' LIMIT 1", 'make_DELETE() preserves column order' ); # Test what happens if the row has a column that not in the tbl struct. $row->{other_col} = 'zzz'; is( $ch->make_INSERT($row, [qw(id foo bar)]), "INSERT INTO `test`.`issue_371`(`id`, `foo`, `bar`, `other_col`) VALUES ('1', 'foo', 'bar', 'zzz')", 'make_INSERT() preserves column order, with col not in tbl' ); is( $ch->make_REPLACE($row, [qw(id foo bar)]), "REPLACE INTO `test`.`issue_371`(`id`, `foo`, `bar`, `other_col`) VALUES ('1', 'foo', 'bar', 'zzz')", 'make_REPLACE() preserves column order, with col not in tbl' ); is( $ch->make_UPDATE($row, [qw(id foo)]), "UPDATE `test`.`issue_371` SET `bar`='bar', `other_col`='zzz' WHERE `id`='1' AND `foo`='foo' LIMIT 1", 'make_UPDATE() preserves column order, with col not in tbl' ); delete $row->{other_col}; SKIP: { skip 'Cannot connect to sandbox master', 3 unless $master_dbh; $master_dbh->do('DROP TABLE IF EXISTS test.issue_371'); $master_dbh->do('CREATE TABLE test.issue_371 (id INT, foo varchar(16), bar char)'); $master_dbh->do("INSERT INTO test.issue_371 VALUES (1,'foo','a'),(2,'bar','b')"); $ch->fetch_back($master_dbh); is( $ch->make_INSERT($row, [qw(id foo)]), "INSERT INTO `test`.`issue_371`(`id`, `foo`, `bar`) VALUES ('1', 'foo', 'a')", 'make_INSERT() preserves column order, with fetch-back' ); is( $ch->make_REPLACE($row, [qw(id foo)]), "REPLACE INTO `test`.`issue_371`(`id`, `foo`, `bar`) VALUES ('1', 'foo', 'a')", 'make_REPLACE() preserves column order, with fetch-back' ); is( $ch->make_UPDATE($row, [qw(id foo)]), "UPDATE `test`.`issue_371` SET `bar`='a' WHERE `id`='1' AND `foo`='foo' LIMIT 1", 'make_UPDATE() preserves column order, with fetch-back' ); }; # ############################################################################# # Issue 641: Make mk-table-sync use hex for binary/blob data # ############################################################################# $tbl_struct = { cols => [qw(a x b)], type_for => {a=>'int', x=>'blob', b=>'varchar'}, }; $ch = new ChangeHandler( Quoter => $q, left_db => 'test', left_tbl => 'lt', right_db => 'test', right_tbl => 'rt', actions => [ sub {} ], replace => 0, queue => 0, tbl_struct => $tbl_struct, ); is( $ch->make_fetch_back_query('1=1'), "SELECT `a`, IF(BINARY(`x`)='', '', CONCAT('0x', HEX(`x`))) AS `x`, `b` FROM `test`.`lt` WHERE 1=1 LIMIT 1", "Wraps BLOB column in CONCAT('0x', HEX(col)) AS col" ); $ch = new ChangeHandler( Quoter => $q, left_db => 'test', left_tbl => 'lt', right_db => 'test', right_tbl => 'rt', actions => [ sub {} ], replace => 0, queue => 0, hex_blob => 0, tbl_struct => $tbl_struct, ); is( $ch->make_fetch_back_query('1=1'), "SELECT `a`, `x`, `b` FROM `test`.`lt` WHERE 1=1 LIMIT 1", "Disable blob hexing" ); # ############################################################################# # Issue 1052: mk-table-sync inserts "0x" instead of "" for empty blob and text # column values # ############################################################################# $tbl_struct = { cols => [qw(t)], type_for => {t=>'blob'}, }; $ch = new ChangeHandler( Quoter => $q, left_db => 'test', left_tbl => 't', right_db => 'test', right_tbl => 't', actions => [ sub {} ], replace => 0, queue => 0, tbl_struct => $tbl_struct, ); is( $ch->make_fetch_back_query('1=1'), "SELECT IF(BINARY(`t`)='', '', CONCAT('0x', HEX(`t`))) AS `t` FROM `test`.`t` WHERE 1=1 LIMIT 1", "Don't prepend 0x to blank blob/text column value (issue 1052)" ); # ############################################################################# # An update to the above bug; It should only hexify for blob and binary, not # for text columns; The latter not only breaks for UTF-8 data, but also # breaks now that hex-looking columns aren't automatically left unquoted. # ############################################################################# $tbl_struct = { cols => [qw(t)], type_for => {t=>'text'}, }; $ch = new ChangeHandler( Quoter => $q, left_db => 'test', left_tbl => 't', right_db => 'test', right_tbl => 't', actions => [ sub {} ], replace => 0, queue => 0, tbl_struct => $tbl_struct, ); is( $ch->make_fetch_back_query('1=1'), "SELECT `t` FROM `test`.`t` WHERE 1=1 LIMIT 1", "Don't prepend 0x to blank blob/text column value (issue 1052)" ); # ############################################################################# SKIP: { skip 'Cannot connect to sandbox master', 1 unless $master_dbh; $sb->load_file('master', "t/lib/samples/issue_641.sql"); @rows = (); $tbl_struct = { cols => [qw(id b)], col_posn => {id=>0, b=>1}, type_for => {id=>'int', b=>'blob'}, }; $ch = new ChangeHandler( Quoter => $q, left_db => 'issue_641', left_tbl => 'lt', right_db => 'issue_641', right_tbl => 'rt', actions => [ sub { push @rows, $_[0]; } ], replace => 0, queue => 0, tbl_struct => $tbl_struct, ); $ch->fetch_back($master_dbh); $ch->change('UPDATE', {id=>1}, [qw(id)] ); $ch->change('INSERT', {id=>1}, [qw(id)] ); is_deeply( \@rows, [ "UPDATE `issue_641`.`rt` SET `b`=0x089504E470D0A1A0A0000000D4948445200000079000000750802000000E55AD965000000097048597300000EC300000EC301C76FA8640000200049444154789C4CBB7794246779FFBBF78F7B7EBE466177677772CE3D9D667AA67BA62776CE39545557CE3974EE9EB049AB9556392210414258083 WHERE `id`='1' LIMIT 1", "INSERT INTO `issue_641`.`rt`(`id`, `b`) VALUES ('1', 0x089504E470D0A1A0A0000000D4948445200000079000000750802000000E55AD965000000097048597300000EC300000EC301C76FA8640000200049444154789C4CBB7794246779FFBBF78F7B7EBE466177677772CE3D9D667AA67BA62776CE39545557CE3974EE9EB049AB9556392210414258083)", ], "UPDATE and INSERT binary data as hex" ); } # ############################################################################# # Issue 387: More useful comments in mk-table-sync statements # ############################################################################# @rows = (); $ch = new ChangeHandler( Quoter => $q, right_db => 'test', # dst right_tbl => 'foo', left_db => 'test', # src left_tbl => 'test1', actions => [ sub { push @rows, $_[0]; push @dbhs, $_[1]; } ], replace => 0, queue => 1, ); $ch->change('INSERT', { a => 1, b => 2 }, [qw(a)] ); $ch->process_rows(1, "trace"); is_deeply( \@rows, ["INSERT INTO `test`.`foo`(`a`, `b`) VALUES ('1', '2') /*percona-toolkit trace*/",], "process_rows() appends trace msg to SQL statements" ); # ############################################################################# # ChangeHandler doesn't quote varchar columns with hex-looking values # https://bugs.launchpad.net/percona-toolkit/+bug/1038276 # ############################################################################# SKIP: { skip 'Cannot connect to sandbox master', 1 unless $master_dbh; $sb->load_file('master', "t/lib/samples/bug_1038276.sql"); @rows = (); $tbl_struct = { cols => [qw(id b)], col_posn => {id=>0, b=>1}, type_for => {id=>'int', b=>'varchar'}, }; $ch = new ChangeHandler( Quoter => $q, left_db => 'bug_1038276', left_tbl => 'lt', right_db => 'bug_1038276', right_tbl => 'rt', actions => [ sub { push @rows, $_[0]; } ], replace => 0, queue => 0, tbl_struct => $tbl_struct, ); $ch->fetch_back($master_dbh); $ch->change('UPDATE', {id=>1}, [qw(id)] ); $ch->change('INSERT', {id=>1}, [qw(id)] ); is_deeply( \@rows, [ "UPDATE `bug_1038276`.`rt` SET `b`='0x89504E470D0A1A0A0000000D4948445200000079000000750802000000E55AD965000000097048597300000EC300000EC301C76FA8640000200049444154789C4CBB7794246779FFBBF78F7B7EBE466177677772CE3D9D667AA67BA62776CE39545557CE3974EE9EB049AB9556392210414258083' WHERE `id`='1' LIMIT 1", "INSERT INTO `bug_1038276`.`rt`(`id`, `b`) VALUES ('1', '0x89504E470D0A1A0A0000000D4948445200000079000000750802000000E55AD965000000097048597300000EC300000EC301C76FA8640000200049444154789C4CBB7794246779FFBBF78F7B7EBE466177677772CE3D9D667AA67BA62776CE39545557CE3974EE9EB049AB9556392210414258083')", ], "UPDATE and INSERT quote data regardless of how it looks if tbl_struct->quote_val is true" ); } # ############################################################################# # Done. # ############################################################################# $sb->wipe_clean($master_dbh); $sb->wipe_clean($slave1_dbh); ok($sb->ok(), "Sandbox servers") or BAIL_OUT(__FILE__ . " broke the sandbox"); done_testing;