4
die "The PERCONA_TOOLKIT_BRANCH environment variable is not set.\n"
5
unless $ENV{PERCONA_TOOLKIT_BRANCH} && -d $ENV{PERCONA_TOOLKIT_BRANCH};
6
unshift @INC, "$ENV{PERCONA_TOOLKIT_BRANCH}/lib";
10
use warnings FATAL => 'all';
11
use English qw(-no_match_vars);
30
my $dp = new DSNParser(opts=>$dsn_opts);
31
my $sb = new Sandbox(basedir => '/tmp', DSNParser => $dp);
32
my $dbh = $sb->get_dbh_for('master');
35
plan skip_all => 'Cannot connect to sandbox master';
41
my $mysql = $sb->_use_for('master');
44
my $ms = new MasterSlave();
45
my $tp = new TableParser(Quoter=>$q);
46
my $du = new MySQLDump();
47
my $vp = new VersionParser();
50
my $nibbler = new TableNibbler(
54
my $checksum = new TableChecksum(
58
my $chunker = new TableChunker(
62
my $t = new TableSyncNibble(
63
TableNibbler => $nibbler,
65
TableChunker => $chunker,
71
my $ch = new ChangeHandler(
78
actions => [ sub { push @rows, $_[0] }, ],
82
my $syncer = new TableSyncer(
84
TableChecksum => $checksum,
90
$sb->create_dbs($dbh, ['test']);
91
diag(`$mysql < $trunk/t/lib/samples/before-TableSyncNibble.sql`);
92
my $ddl = $du->get_create_table($dbh, $q, 'test', 'test1');
93
my $tbl_struct = $tp->parse($ddl);
110
tbl_struct => $tbl_struct,
111
cols => $tbl_struct->{cols},
113
chunk_index => 'PRIMARY',
114
key_cols => $tbl_struct->{keys}->{PRIMARY}->{cols},
116
index_hint => 'USE INDEX (`PRIMARY`)',
117
ChangeHandler => $ch,
120
$t->prepare_to_sync(%args);
121
# Test with FNV_64 just to make sure there are no errors
122
eval { $dbh->do('select fnv_64(1)') };
124
skip 'No FNV_64 function installed', 1 if $EVAL_ERROR;
126
$t->set_checksum_queries(
127
$syncer->make_checksum_queries(%args, function => 'FNV_64')
134
q{SELECT /*test.test1:1/1*/ 0 AS chunk_num, COUNT(*) AS }
135
. q{cnt, COALESCE(LOWER(CONV(BIT_XOR(CAST(FNV_64(`a`, `b`, `c`) AS UNSIGNED)), }
136
. q{10, 16)), 0) AS crc FROM `test`.`test1` USE INDEX (`PRIMARY`) WHERE (((`a` < '1') OR (`a` = '1' }
137
. q{AND `b` <= 'en')))},
138
'First nibble SQL with FNV_64',
142
$t->set_checksum_queries(
143
$syncer->make_checksum_queries(%args, function => 'SHA1')
150
($sandbox_version gt '4.0' ?
151
q{SELECT /*test.test1:1/1*/ 0 AS chunk_num, COUNT(*) AS cnt, }
152
. q{COALESCE(LOWER(CONCAT(LPAD(CONV(BIT_XOR(CAST(CONV(SUBSTRING(@crc, 1, 16), 16, }
153
. q{10) AS UNSIGNED)), 10, 16), 16, '0'), LPAD(CONV(BIT_XOR(CAST(CONV(}
154
. q{SUBSTRING(@crc, 17, 16), 16, 10) AS UNSIGNED)), 10, 16), 16, '0'), }
155
. q{LPAD(CONV(BIT_XOR(CAST(CONV(SUBSTRING(@crc := SHA1(CONCAT_WS('#', `a`, }
156
. q{`b`, `c`)), 33, 8), 16, 10) AS UNSIGNED)), 10, 16), 8, '0'))), 0) AS crc FROM }
157
. q{`test`.`test1` USE INDEX (`PRIMARY`) WHERE (((`a` < '1') OR (`a` = '1' AND `b` <= 'en')))} :
158
q{SELECT /*test.test1:1/1*/ 0 AS chunk_num, COUNT(*) AS cnt, }
159
. q{COALESCE(RIGHT(MAX(@crc := CONCAT(LPAD(@cnt := @cnt + 1, 16, '0'), }
160
. q{SHA1(CONCAT(@crc, SHA1(CONCAT_WS('#', `a`, `b`, `c`)))))), 40), 0) AS crc FROM }
161
. q{`test`.`test1` USE INDEX (`PRIMARY`) WHERE (((`a` < '1') OR (`a` = '1' AND `b` <= 'en')))}
171
($sandbox_version gt '4.0' ?
172
q{SELECT /*test.test1:1/1*/ 0 AS chunk_num, COUNT(*) AS cnt, }
173
. q{COALESCE(LOWER(CONCAT(LPAD(CONV(BIT_XOR(CAST(CONV(SUBSTRING(@crc, 1, 16), 16, }
174
. q{10) AS UNSIGNED)), 10, 16), 16, '0'), LPAD(CONV(BIT_XOR(CAST(CONV(}
175
. q{SUBSTRING(@crc, 17, 16), 16, 10) AS UNSIGNED)), 10, 16), 16, '0'), }
176
. q{LPAD(CONV(BIT_XOR(CAST(CONV(SUBSTRING(@crc := SHA1(CONCAT_WS('#', `a`, }
177
. q{`b`, `c`)), 33, 8), 16, 10) AS UNSIGNED)), 10, 16), 8, '0'))), 0) AS crc FROM }
178
. q{`test`.`test1` USE INDEX (`PRIMARY`) WHERE (((`a` < '1') OR (`a` = '1' AND `b` <= 'en')))} :
179
q{SELECT /*test.test1:1/1*/ 0 AS chunk_num, COUNT(*) AS cnt, }
180
. q{COALESCE(RIGHT(MAX(@crc := CONCAT(LPAD(@cnt := @cnt + 1, 16, '0'), }
181
. q{SHA1(CONCAT(@crc, SHA1(CONCAT_WS('#', `a`, `b`, `c`)))))), 40), 0) AS crc FROM }
182
. q{`test`.`test1` USE INDEX (`PRIMARY`) WHERE (((`a` < '1') OR (`a` = '1' AND `b` <= 'en')))}
184
'First nibble SQL, again',
188
delete $t->{cached_boundaries};
195
($sandbox_version gt '4.0' ?
196
q{SELECT /*test.test1:1/1*/ 0 AS chunk_num, COUNT(*) AS cnt, }
197
. q{COALESCE(LOWER(CONCAT(LPAD(CONV(BIT_XOR(CAST(CONV(SUBSTRING(@crc, 1, 16), 16, }
198
. q{10) AS UNSIGNED)), 10, 16), 16, '0'), LPAD(CONV(BIT_XOR(CAST(CONV(}
199
. q{SUBSTRING(@crc, 17, 16), 16, 10) AS UNSIGNED)), 10, 16), 16, '0'), }
200
. q{LPAD(CONV(BIT_XOR(CAST(CONV(SUBSTRING(@crc := SHA1(CONCAT_WS('#', `a`, }
201
. q{`b`, `c`)), 33, 8), 16, 10) AS UNSIGNED)), 10, 16), 8, '0'))), 0) AS crc FROM }
202
. q{`test`.`test1` USE INDEX (`PRIMARY`) WHERE ((((`a` > '1') OR (`a` = '1' AND `b` > 'en')) AND }
203
. q{((`a` < '2') OR (`a` = '2' AND `b` <= 'ca'))))} :
204
q{SELECT /*test.test1:1/1*/ 0 AS chunk_num, COUNT(*) AS cnt, }
205
. q{COALESCE(RIGHT(MAX(@crc := CONCAT(LPAD(@cnt := @cnt + 1, 16, '0'), }
206
. q{SHA1(CONCAT(@crc, SHA1(CONCAT_WS('#', `a`, `b`, `c`)))))), 40), 0) AS crc FROM }
207
. q{`test`.`test1` USE INDEX (`PRIMARY`) WHERE ((((`a` > '1') OR (`a` = '1' AND `b` > 'en')) AND }
208
. q{((`a` < '2') OR (`a` = '2' AND `b` <= 'ca'))))}
213
# Bump the nibble boundaries ahead until we run off the end of the table.
214
$t->done_with_rows();
219
$t->done_with_rows();
224
$t->done_with_rows();
235
($sandbox_version gt '4.0' ?
236
q{SELECT /*test.test1:1/1*/ 0 AS chunk_num, COUNT(*) AS cnt, }
237
. q{COALESCE(LOWER(CONCAT(LPAD(CONV(BIT_XOR(CAST(CONV(SUBSTRING(@crc, 1, 16), 16, }
238
. q{10) AS UNSIGNED)), 10, 16), 16, '0'), LPAD(CONV(BIT_XOR(CAST(CONV(}
239
. q{SUBSTRING(@crc, 17, 16), 16, 10) AS UNSIGNED)), 10, 16), 16, '0'), }
240
. q{LPAD(CONV(BIT_XOR(CAST(CONV(SUBSTRING(@crc := SHA1(CONCAT_WS('#', `a`, }
241
. q{`b`, `c`)), 33, 8), 16, 10) AS UNSIGNED)), 10, 16), 8, '0'))), 0) AS crc FROM }
242
. q{`test`.`test1` USE INDEX (`PRIMARY`) WHERE ((((`a` > '4') OR (`a` = '4' AND `b` > 'bz')) AND }
244
q{SELECT /*test.test1:1/1*/ 0 AS chunk_num, COUNT(*) AS cnt, }
245
. q{COALESCE(RIGHT(MAX(@crc := CONCAT(LPAD(@cnt := @cnt + 1, 16, '0'), }
246
. q{SHA1(CONCAT(@crc, SHA1(CONCAT_WS('#', `a`, `b`, `c`)))))), 40), 0) AS crc FROM }
247
. q{`test`.`test1` USE INDEX (`PRIMARY`) WHERE ((((`a` > '4') OR (`a` = '4' AND `b` > 'bz')) AND }
250
'End-of-table nibble SQL',
253
$t->done_with_rows();
254
ok($t->done(), 'Now done');
256
# Throw away and start anew, because it's off the end of the table
258
delete $t->{cached_boundaries};
259
delete $t->{cached_nibble};
260
delete $t->{cached_row};
262
is_deeply($t->key_cols(), [qw(chunk_num)], 'Key cols in state 0');
267
$t->done_with_rows();
269
is($t->done(), '', 'Not done, because not reached end-of-table');
272
sub { $t->not_in_left() },
274
'not_in_(side) illegal in state 0',
277
# Now "find some bad chunks," as it were.
281
lr => { chunk_num => 0, cnt => 0, crc => 'abc' },
282
rr => { chunk_num => 0, cnt => 1, crc => 'abc' },
284
ok($t->pending_changes(), 'Pending changes found');
285
is($t->{state}, 1, 'Working inside nibble');
286
$t->done_with_rows();
287
is($t->{state}, 2, 'Now in state to fetch individual rows');
288
ok($t->pending_changes(), 'Pending changes not done yet');
289
is($t->get_sql(database => 'test', table => 'test1'),
290
q{SELECT /*rows in nibble*/ `a`, `b`, `c`, SHA1(CONCAT_WS('#', `a`, `b`, `c`)) AS __crc FROM }
291
. q{`test`.`test1` USE INDEX (`PRIMARY`) WHERE ((((`a` > '1') OR (`a` = '1' AND `b` > 'en')) }
292
. q{AND ((`a` < '2') OR (`a` = '2' AND `b` <= 'ca'))))}
293
. q{ ORDER BY `a`, `b`},
294
'SQL now working inside nibble'
296
ok($t->{state}, 'Still working inside nibble');
297
is(scalar(@rows), 0, 'No bad row triggered');
299
$t->not_in_left(rr => {a => 1, b => 'en'});
302
["DELETE FROM `test`.`test1` WHERE `a`='1' AND `b`='en' LIMIT 1"],
303
'Working inside nibble, got a bad row',
306
# Shouldn't cause anything to happen
308
lr => {a => 1, b => 'en', __crc => 'foo'},
309
rr => {a => 1, b => 'en', __crc => 'foo'} );
312
["DELETE FROM `test`.`test1` WHERE `a`='1' AND `b`='en' LIMIT 1"],
313
'No more rows added',
317
lr => {a => 1, b => 'en', __crc => 'foo'},
318
rr => {a => 1, b => 'en', __crc => 'bar'} );
322
"DELETE FROM `test`.`test1` WHERE `a`='1' AND `b`='en' LIMIT 1",
323
"UPDATE `test`.`test1` SET `c`='a' WHERE `a`='1' AND `b`='en' LIMIT 1",
325
'Row added to update differing row',
328
$t->done_with_rows();
329
is($t->{state}, 0, 'Now not working inside nibble');
330
is($t->pending_changes(), 0, 'No pending changes');
332
# Now test that SQL_BUFFER_RESULT is in the queries OK
333
$t->prepare_to_sync(%args, buffer_in_mysql=>1);
339
buffer_in_mysql => 1,
341
qr/SELECT ..rows in nibble.. SQL_BUFFER_RESULT/,
342
'Buffering in first nibble',
347
lr => { chunk_num => 0, cnt => 0, __crc => 'abc' },
348
rr => { chunk_num => 0, cnt => 1, __crc => 'abc' },
355
buffer_in_mysql => 1,
357
qr/SELECT ..rows in nibble.. SQL_BUFFER_RESULT/,
358
'Buffering in next nibble',
361
# #########################################################################
362
# Issue 96: mk-table-sync: Nibbler infinite loop
363
# #########################################################################
364
$sb->load_file('master', 't/lib/samples/issue_96.sql');
365
$tbl_struct = $tp->parse($du->get_create_table($dbh, $q, 'issue_96', 't'));
367
ChangeHandler => $ch,
368
cols => $tbl_struct->{cols},
372
tbl_struct => $tbl_struct,
374
chunk_index => 'package_id',
375
crc_col => '__crc_col',
376
index_hint => 'FORCE INDEX(`package_id`)',
377
key_cols => $tbl_struct->{keys}->{package_id}->{cols},
380
# Test that we die if MySQL isn't using the chosen index (package_id)
381
# for the boundary sql.
383
my $sql = "SELECT /*nibble boundary 0*/ `package_id`,`location`,`from_city` FROM `issue_96`.`t` FORCE INDEX(`package_id`) ORDER BY `package_id`,`location` LIMIT 1, 1";
385
$t->__get_explain_index($sql),
387
'__get_explain_index()'
390
diag(`/tmp/12345/use -e 'ALTER TABLE issue_96.t DROP INDEX package_id'`);
393
$t->__get_explain_index($sql),
395
'__get_explain_index() for nonexistent index'
398
my %args2 = ( database=>'issue_96', table=>'t' );
400
$t->get_sql(database=>'issue_96', tbl=>'t', %args2);
404
qr/^Cannot nibble table `issue_96`.`t` because MySQL chose no index instead of the `package_id` index/,
405
"Die if MySQL doesn't choose our index (issue 96)"
408
# Restore the index, get the first sql boundary and check that it
409
# has the proper ORDER BY clause which makes MySQL use the index.
410
diag(`/tmp/12345/use -e 'ALTER TABLE issue_96.t ADD UNIQUE INDEX package_id (package_id,location);'`);
412
($sql,undef) = $t->__make_boundary_sql(%args2);
416
"SELECT /*nibble boundary 0*/ `package_id`,`location`,`from_city` FROM `issue_96`.`t` FORCE INDEX(`package_id`) ORDER BY `package_id`,`location` LIMIT 1, 1",
417
'Boundary SQL has ORDER BY key columns'
420
# If small_table is true, the index check should be skipped.
421
diag(`/tmp/12345/use -e 'create table issue_96.t3 (i int, unique index (i))'`);
422
diag(`/tmp/12345/use -e 'insert into issue_96.t3 values (1)'`);
423
$tbl_struct = $tp->parse($du->get_create_table($dbh, $q, 'issue_96', 't3'));
425
ChangeHandler => $ch,
426
cols => $tbl_struct->{cols},
430
tbl_struct => $tbl_struct,
433
crc_col => '__crc_col',
434
index_hint => 'FORCE INDEX(`i`)',
435
key_cols => $tbl_struct->{keys}->{i}->{cols},
439
$t->get_sql(database=>'issue_96', table=>'t3');
444
"Skips index check when small table (issue 634)"
447
my ($can_sync, %plugin_args);
449
skip "Not tested on MySQL $sandbox_version", 5
450
unless $sandbox_version gt '4.0';
452
# #############################################################################
453
# Issue 560: mk-table-sync generates impossible WHERE
454
# Issue 996: might not chunk inside of mk-table-checksum's boundaries
455
# #############################################################################
456
# Due to issue 996 this test has changed. Now it *should* use the replicate
457
# boundary provided via the where arg and nibble just inside this boundary.
458
# If it does, then it will prevent the impossible WHERE of issue 560.
460
# The buddy_list table has 500 rows, so when it's chunk into 100 rows this is
462
my $where = '`player_id` >= 201 AND `player_id` < 301';
464
$sb->load_file('master', 't/pt-table-sync/samples/issue_560.sql');
465
$tbl_struct = $tp->parse($du->get_create_table($dbh, $q, 'issue_560', 'buddy_list'));
466
(undef, %plugin_args) = $t->can_sync(tbl_struct => $tbl_struct);
468
ChangeHandler => $ch,
469
cols => $tbl_struct->{cols},
473
tbl_struct => $tbl_struct,
475
crc_col => '__crc_col',
477
replicate => 'issue_560.checksum',
478
where => $where, # not used in sub but normally passed so we
479
# do the same to simulate a real run
482
# Must call this else $row_sql will have values from previous test.
483
$t->set_checksum_queries(
484
$syncer->make_checksum_queries(
487
tbl_struct => $tbl_struct,
494
database => 'issue_560',
495
table => 'buddy_list',
497
"SELECT /*issue_560.buddy_list:1/1*/ 0 AS chunk_num, COUNT(*) AS cnt, COALESCE(LOWER(CONV(BIT_XOR(CAST(CRC32(CONCAT_WS('#', `player_id`, `buddy_id`)) AS UNSIGNED)), 10, 16)), 0) AS crc FROM `issue_560`.`buddy_list` WHERE (((`player_id` < '300') OR (`player_id` = '300' AND `buddy_id` <= '2085'))) AND (($where))",
498
'Nibble with chunk boundary (chunk sql)'
505
database => 'issue_560',
506
table => 'buddy_list',
508
"SELECT /*rows in nibble*/ `player_id`, `buddy_id`, CRC32(CONCAT_WS('#', `player_id`, `buddy_id`)) AS __crc_col FROM `issue_560`.`buddy_list` WHERE (((`player_id` < '300') OR (`player_id` = '300' AND `buddy_id` <= '2085'))) AND ($where) ORDER BY `player_id`, `buddy_id`",
509
'Nibble with chunk boundary (row sql)'
513
$t->done_with_rows();
517
database => 'issue_560',
518
table => 'buddy_list',
520
"SELECT /*issue_560.buddy_list:1/1*/ 0 AS chunk_num, COUNT(*) AS cnt, COALESCE(LOWER(CONV(BIT_XOR(CAST(CRC32(CONCAT_WS('#', `player_id`, `buddy_id`)) AS UNSIGNED)), 10, 16)), 0) AS crc FROM `issue_560`.`buddy_list` WHERE ((((`player_id` > '300') OR (`player_id` = '300' AND `buddy_id` > '2085')) AND 1=1)) AND (($where))",
524
# Just like the previous tests but this time the chunk size is 50 so we
525
# should nibble two chunks within the larger range ($where).
527
ChangeHandler => $ch,
528
cols => $tbl_struct->{cols},
532
tbl_struct => $tbl_struct,
533
chunk_size => 50, # 2 sub-nibbles
534
crc_col => '__crc_col',
536
replicate => 'issue_560.checksum',
537
where => $where, # not used in sub but normally passed so we
538
# do the same to simulate a real run
541
# Must call this else $row_sql will have values from previous test.
542
$t->set_checksum_queries(
543
$syncer->make_checksum_queries(
546
tbl_struct => $tbl_struct,
553
database => 'issue_560',
554
table => 'buddy_list',
556
"SELECT /*issue_560.buddy_list:1/1*/ 0 AS chunk_num, COUNT(*) AS cnt, COALESCE(LOWER(CONV(BIT_XOR(CAST(CRC32(CONCAT_WS('#', `player_id`, `buddy_id`)) AS UNSIGNED)), 10, 16)), 0) AS crc FROM `issue_560`.`buddy_list` WHERE (((`player_id` < '250') OR (`player_id` = '250' AND `buddy_id` <= '809'))) AND ((`player_id` >= 201 AND `player_id` < 301))",
560
$t->done_with_rows();
564
database => 'issue_560',
565
table => 'buddy_list',
567
"SELECT /*issue_560.buddy_list:1/1*/ 0 AS chunk_num, COUNT(*) AS cnt, COALESCE(LOWER(CONV(BIT_XOR(CAST(CRC32(CONCAT_WS('#', `player_id`, `buddy_id`)) AS UNSIGNED)), 10, 16)), 0) AS crc FROM `issue_560`.`buddy_list` WHERE ((((`player_id` > '250') OR (`player_id` = '250' AND `buddy_id` > '809')) AND ((`player_id` < '300') OR (`player_id` = '300' AND `buddy_id` <= '2085')))) AND ((`player_id` >= 201 AND `player_id` < 301))",
572
# #############################################################################
573
# Issue 804: mk-table-sync: can't nibble because index name isn't lower case?
574
# #############################################################################
575
$sb->load_file('master', 't/lib/samples/issue_804.sql');
576
$tbl_struct = $tp->parse($du->get_create_table($dbh, $q, 'issue_804', 't'));
577
($can_sync, %plugin_args) = $t->can_sync(tbl_struct => $tbl_struct);
581
'Can sync issue_804 table'
586
chunk_index => 'purchases_accountid_purchaseid',
587
key_cols => [qw(accountid purchaseid)],
590
'Plugin args for issue_804 table'
594
ChangeHandler => $ch,
595
cols => $tbl_struct->{cols},
599
tbl_struct => $tbl_struct,
601
chunk_index => $plugin_args{chunk_index},
602
crc_col => '__crc_col',
603
index_hint => 'FORCE INDEX(`'.$plugin_args{chunk_index}.'`)',
604
key_cols => $tbl_struct->{keys}->{$plugin_args{chunk_index}}->{cols},
607
# Must call this else $row_sql will have values from previous test.
608
$t->set_checksum_queries(
609
$syncer->make_checksum_queries(
612
tbl_struct => $tbl_struct,
616
# Before fixing issue 804, the code would die during this call, saying:
617
# Cannot nibble table `issue_804`.`t` because MySQL chose the
618
# `purchases_accountId_purchaseId` index instead of the
619
# `purchases_accountid_purchaseid` index at TableSyncNibble.pm line 284.
620
$sql = $t->get_sql(database=>'issue_804', table=>'t');
623
($sandbox_version gt '4.0' ?
624
"SELECT /*issue_804.t:1/1*/ 0 AS chunk_num, COUNT(*) AS cnt, COALESCE(LOWER(CONV(BIT_XOR(CAST(CRC32(CONCAT_WS('#', `accountid`, `purchaseid`)) AS UNSIGNED)), 10, 16)), 0) AS crc FROM `issue_804`.`t` FORCE INDEX(`purchases_accountid_purchaseid`) WHERE (((`accountid` < '49') OR (`accountid` = '49' AND `purchaseid` <= '50')))" :
625
"SELECT /*issue_804.t:1/1*/ 0 AS chunk_num, COUNT(*) AS cnt, COALESCE(RIGHT(MAX(\@crc := CONCAT(LPAD(\@cnt := \@cnt + 1, 16, '0'), MD5(CONCAT(\@crc, MD5(CONCAT_WS('#', `accountid`, `purchaseid`)))))), 32), 0) AS crc FROM `issue_804`.`t` FORCE INDEX(`purchases_accountid_purchaseid`) WHERE (((`accountid` < '49') OR (`accountid` = '49' AND `purchaseid` <= '50')))"
627
'SQL nibble for issue_804 table'
630
# #############################################################################
632
# #############################################################################
633
$sb->wipe_clean($dbh);