Merge lp:~percona-toolkit-dev/percona-toolkit/pt-osc-2.1.1 into lp:percona-toolkit/2.1
- pt-osc-2.1.1
- Merge into 2.1
Proposed by
Daniel Nichter
Status: | Merged | ||||||||
---|---|---|---|---|---|---|---|---|---|
Merged at revision: | 228 | ||||||||
Proposed branch: | lp:~percona-toolkit-dev/percona-toolkit/pt-osc-2.1.1 | ||||||||
Merge into: | lp:percona-toolkit/2.1 | ||||||||
Diff against target: |
9987 lines (+6370/-2594) (has conflicts) 24 files modified
bin/pt-online-schema-change (+4615/-1243) bin/pt-table-checksum (+114/-37) lib/CleanupTask.pm (+7/-2) lib/MySQLStatusWaiter.pm (+34/-12) lib/NibbleIterator.pm (+165/-49) lib/OSCCaptureSync.pm (+0/-142) lib/OobNibbleIterator.pm (+1/-1) lib/SchemaIterator.pm (+12/-0) t/lib/CleanupTask.t (+18/-1) t/lib/MySQLStatusWaiter.t (+38/-4) t/lib/NibbleIterator.t (+42/-4) t/lib/OSCCaptureSync.t (+0/-131) t/lib/OobNibbleIterator.t (+8/-3) t/pt-online-schema-change/alter_active_table.t (+56/-46) t/pt-online-schema-change/basics.t (+541/-218) t/pt-online-schema-change/check_tables.t (+0/-126) t/pt-online-schema-change/option_sanity.t (+12/-12) t/pt-online-schema-change/samples/basic_no_fks.data (+500/-500) t/pt-online-schema-change/samples/basic_no_fks.sql (+30/-0) t/pt-online-schema-change/samples/basic_with_fks.sql (+56/-0) t/pt-online-schema-change/samples/fk_tables_schema.sql (+0/-31) t/pt-online-schema-change/samples/query_table.pl (+8/-5) t/pt-online-schema-change/samples/small_table.sql (+0/-27) t/pt-online-schema-change/sanity_checks.t (+113/-0) Text conflict in bin/pt-online-schema-change Text conflict in bin/pt-table-checksum Text conflict in lib/NibbleIterator.pm Text conflict in lib/SchemaIterator.pm |
||||||||
To merge this branch: | bzr merge lp:~percona-toolkit-dev/percona-toolkit/pt-osc-2.1.1 | ||||||||
Related bugs: |
|
||||||||
Related blueprints: |
Redesign pt-online-schema-change
(Essential)
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Baron Schwartz (community) | Approve | ||
Daniel Nichter | Approve | ||
Brian Fraser | Pending | ||
Review via email: mp+100540@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
Revision history for this message
Daniel Nichter (daniel-nichter) : | # |
review:
Approve
Revision history for this message
Baron Schwartz (baron-xaprb) : | # |
review:
Approve
- 251. By Daniel Nichter
-
Add comment about code that will fail with new Cxn due to NAME_lc.
- 252. By Daniel Nichter
-
Catch lost cleanup task (should rarely happen).
- 253. By Daniel Nichter
-
Handle Key_name or key_name in NibbleIterator:
:_get_index_ cardinality( ). - 254. By Daniel Nichter
-
Add XXX comment about improperly copying .
- 255. By Daniel Nichter
-
Uncomment cleanup lines in alter_active_
table.t. - 256. By Daniel Nichter
-
Update modules in pt-online-
schema- change. - 257. By Daniel Nichter
-
Fix typo.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'bin/pt-online-schema-change' |
2 | --- bin/pt-online-schema-change 2012-03-30 16:37:16 +0000 |
3 | +++ bin/pt-online-schema-change 2012-04-03 02:13:24 +0000 |
4 | @@ -1784,245 +1784,246 @@ |
5 | # ########################################################################### |
6 | |
7 | # ########################################################################### |
8 | -# Transformers package |
9 | +# TableNibbler package |
10 | # This package is a copy without comments from the original. The original |
11 | # with comments and its test file can be found in the Bazaar repository at, |
12 | -# lib/Transformers.pm |
13 | -# t/lib/Transformers.t |
14 | +# lib/TableNibbler.pm |
15 | +# t/lib/TableNibbler.t |
16 | # See https://launchpad.net/percona-toolkit for more information. |
17 | # ########################################################################### |
18 | { |
19 | -package Transformers; |
20 | +package TableNibbler; |
21 | |
22 | use strict; |
23 | use warnings FATAL => 'all'; |
24 | use English qw(-no_match_vars); |
25 | use constant PTDEBUG => $ENV{PTDEBUG} || 0; |
26 | |
27 | -use Time::Local qw(timegm timelocal); |
28 | -use Digest::MD5 qw(md5_hex); |
29 | - |
30 | -require Exporter; |
31 | -our @ISA = qw(Exporter); |
32 | -our %EXPORT_TAGS = (); |
33 | -our @EXPORT = (); |
34 | -our @EXPORT_OK = qw( |
35 | - micro_t |
36 | - percentage_of |
37 | - secs_to_time |
38 | - time_to_secs |
39 | - shorten |
40 | - ts |
41 | - parse_timestamp |
42 | - unix_timestamp |
43 | - any_unix_timestamp |
44 | - make_checksum |
45 | - crc32 |
46 | -); |
47 | - |
48 | -our $mysql_ts = qr/(\d\d)(\d\d)(\d\d) +(\d+):(\d+):(\d+)(\.\d+)?/; |
49 | -our $proper_ts = qr/(\d\d\d\d)-(\d\d)-(\d\d)[T ](\d\d):(\d\d):(\d\d)(\.\d+)?/; |
50 | -our $n_ts = qr/(\d{1,5})([shmd]?)/; # Limit \d{1,5} because \d{6} looks |
51 | - |
52 | -sub micro_t { |
53 | - my ( $t, %args ) = @_; |
54 | - my $p_ms = defined $args{p_ms} ? $args{p_ms} : 0; # precision for ms vals |
55 | - my $p_s = defined $args{p_s} ? $args{p_s} : 0; # precision for s vals |
56 | - my $f; |
57 | - |
58 | - $t = 0 if $t < 0; |
59 | - |
60 | - $t = sprintf('%.17f', $t) if $t =~ /e/; |
61 | - |
62 | - $t =~ s/\.(\d{1,6})\d*/\.$1/; |
63 | - |
64 | - if ($t > 0 && $t <= 0.000999) { |
65 | - $f = ($t * 1000000) . 'us'; |
66 | - } |
67 | - elsif ($t >= 0.001000 && $t <= 0.999999) { |
68 | - $f = sprintf("%.${p_ms}f", $t * 1000); |
69 | - $f = ($f * 1) . 'ms'; # * 1 to remove insignificant zeros |
70 | - } |
71 | - elsif ($t >= 1) { |
72 | - $f = sprintf("%.${p_s}f", $t); |
73 | - $f = ($f * 1) . 's'; # * 1 to remove insignificant zeros |
74 | - } |
75 | - else { |
76 | - $f = 0; # $t should = 0 at this point |
77 | - } |
78 | - |
79 | - return $f; |
80 | -} |
81 | - |
82 | -sub percentage_of { |
83 | - my ( $is, $of, %args ) = @_; |
84 | - my $p = $args{p} || 0; # float precision |
85 | - my $fmt = $p ? "%.${p}f" : "%d"; |
86 | - return sprintf $fmt, ($is * 100) / ($of ||= 1); |
87 | -} |
88 | - |
89 | -sub secs_to_time { |
90 | - my ( $secs, $fmt ) = @_; |
91 | - $secs ||= 0; |
92 | - return '00:00' unless $secs; |
93 | - |
94 | - $fmt ||= $secs >= 86_400 ? 'd' |
95 | - : $secs >= 3_600 ? 'h' |
96 | - : 'm'; |
97 | - |
98 | - return |
99 | - $fmt eq 'd' ? sprintf( |
100 | - "%d+%02d:%02d:%02d", |
101 | - int($secs / 86_400), |
102 | - int(($secs % 86_400) / 3_600), |
103 | - int(($secs % 3_600) / 60), |
104 | - $secs % 60) |
105 | - : $fmt eq 'h' ? sprintf( |
106 | - "%02d:%02d:%02d", |
107 | - int(($secs % 86_400) / 3_600), |
108 | - int(($secs % 3_600) / 60), |
109 | - $secs % 60) |
110 | - : sprintf( |
111 | - "%02d:%02d", |
112 | - int(($secs % 3_600) / 60), |
113 | - $secs % 60); |
114 | -} |
115 | - |
116 | -sub time_to_secs { |
117 | - my ( $val, $default_suffix ) = @_; |
118 | - die "I need a val argument" unless defined $val; |
119 | - my $t = 0; |
120 | - my ( $prefix, $num, $suffix ) = $val =~ m/([+-]?)(\d+)([a-z])?$/; |
121 | - $suffix = $suffix || $default_suffix || 's'; |
122 | - if ( $suffix =~ m/[smhd]/ ) { |
123 | - $t = $suffix eq 's' ? $num * 1 # Seconds |
124 | - : $suffix eq 'm' ? $num * 60 # Minutes |
125 | - : $suffix eq 'h' ? $num * 3600 # Hours |
126 | - : $num * 86400; # Days |
127 | - |
128 | - $t *= -1 if $prefix && $prefix eq '-'; |
129 | - } |
130 | - else { |
131 | - die "Invalid suffix for $val: $suffix"; |
132 | - } |
133 | - return $t; |
134 | -} |
135 | - |
136 | -sub shorten { |
137 | - my ( $num, %args ) = @_; |
138 | - my $p = defined $args{p} ? $args{p} : 2; # float precision |
139 | - my $d = defined $args{d} ? $args{d} : 1_024; # divisor |
140 | - my $n = 0; |
141 | - my @units = ('', qw(k M G T P E Z Y)); |
142 | - while ( $num >= $d && $n < @units - 1 ) { |
143 | - $num /= $d; |
144 | - ++$n; |
145 | - } |
146 | - return sprintf( |
147 | - $num =~ m/\./ || $n |
148 | - ? "%.${p}f%s" |
149 | - : '%d', |
150 | - $num, $units[$n]); |
151 | -} |
152 | - |
153 | -sub ts { |
154 | - my ( $time, $gmt ) = @_; |
155 | - my ( $sec, $min, $hour, $mday, $mon, $year ) |
156 | - = $gmt ? gmtime($time) : localtime($time); |
157 | - $mon += 1; |
158 | - $year += 1900; |
159 | - my $val = sprintf("%d-%02d-%02dT%02d:%02d:%02d", |
160 | - $year, $mon, $mday, $hour, $min, $sec); |
161 | - if ( my ($us) = $time =~ m/(\.\d+)$/ ) { |
162 | - $us = sprintf("%.6f", $us); |
163 | - $us =~ s/^0\././; |
164 | - $val .= $us; |
165 | - } |
166 | - return $val; |
167 | -} |
168 | - |
169 | -sub parse_timestamp { |
170 | - my ( $val ) = @_; |
171 | - if ( my($y, $m, $d, $h, $i, $s, $f) |
172 | - = $val =~ m/^$mysql_ts$/ ) |
173 | - { |
174 | - return sprintf "%d-%02d-%02d %02d:%02d:" |
175 | - . (defined $f ? '%09.6f' : '%02d'), |
176 | - $y + 2000, $m, $d, $h, $i, (defined $f ? $s + $f : $s); |
177 | - } |
178 | - return $val; |
179 | -} |
180 | - |
181 | -sub unix_timestamp { |
182 | - my ( $val, $gmt ) = @_; |
183 | - if ( my($y, $m, $d, $h, $i, $s, $us) = $val =~ m/^$proper_ts$/ ) { |
184 | - $val = $gmt |
185 | - ? timegm($s, $i, $h, $d, $m - 1, $y) |
186 | - : timelocal($s, $i, $h, $d, $m - 1, $y); |
187 | - if ( defined $us ) { |
188 | - $us = sprintf('%.6f', $us); |
189 | - $us =~ s/^0\././; |
190 | - $val .= $us; |
191 | - } |
192 | - } |
193 | - return $val; |
194 | -} |
195 | - |
196 | -sub any_unix_timestamp { |
197 | - my ( $val, $callback ) = @_; |
198 | - |
199 | - if ( my ($n, $suffix) = $val =~ m/^$n_ts$/ ) { |
200 | - $n = $suffix eq 's' ? $n # Seconds |
201 | - : $suffix eq 'm' ? $n * 60 # Minutes |
202 | - : $suffix eq 'h' ? $n * 3600 # Hours |
203 | - : $suffix eq 'd' ? $n * 86400 # Days |
204 | - : $n; # default: Seconds |
205 | - PTDEBUG && _d('ts is now - N[shmd]:', $n); |
206 | - return time - $n; |
207 | - } |
208 | - elsif ( $val =~ m/^\d{9,}/ ) { |
209 | - PTDEBUG && _d('ts is already a unix timestamp'); |
210 | - return $val; |
211 | - } |
212 | - elsif ( my ($ymd, $hms) = $val =~ m/^(\d{6})(?:\s+(\d+:\d+:\d+))?/ ) { |
213 | - PTDEBUG && _d('ts is MySQL slow log timestamp'); |
214 | - $val .= ' 00:00:00' unless $hms; |
215 | - return unix_timestamp(parse_timestamp($val)); |
216 | - } |
217 | - elsif ( ($ymd, $hms) = $val =~ m/^(\d{4}-\d\d-\d\d)(?:[T ](\d+:\d+:\d+))?/) { |
218 | - PTDEBUG && _d('ts is properly formatted timestamp'); |
219 | - $val .= ' 00:00:00' unless $hms; |
220 | - return unix_timestamp($val); |
221 | - } |
222 | - else { |
223 | - PTDEBUG && _d('ts is MySQL expression'); |
224 | - return $callback->($val) if $callback && ref $callback eq 'CODE'; |
225 | - } |
226 | - |
227 | - PTDEBUG && _d('Unknown ts type:', $val); |
228 | - return; |
229 | -} |
230 | - |
231 | -sub make_checksum { |
232 | - my ( $val ) = @_; |
233 | - my $checksum = uc substr(md5_hex($val), -16); |
234 | - PTDEBUG && _d($checksum, 'checksum for', $val); |
235 | - return $checksum; |
236 | -} |
237 | - |
238 | -sub crc32 { |
239 | - my ( $string ) = @_; |
240 | - return unless $string; |
241 | - my $poly = 0xEDB88320; |
242 | - my $crc = 0xFFFFFFFF; |
243 | - foreach my $char ( split(//, $string) ) { |
244 | - my $comp = ($crc ^ ord($char)) & 0xFF; |
245 | - for ( 1 .. 8 ) { |
246 | - $comp = $comp & 1 ? $poly ^ ($comp >> 1) : $comp >> 1; |
247 | - } |
248 | - $crc = (($crc >> 8) & 0x00FFFFFF) ^ $comp; |
249 | - } |
250 | - return $crc ^ 0xFFFFFFFF; |
251 | +sub new { |
252 | + my ( $class, %args ) = @_; |
253 | + my @required_args = qw(TableParser Quoter); |
254 | + foreach my $arg ( @required_args ) { |
255 | + die "I need a $arg argument" unless $args{$arg}; |
256 | + } |
257 | + my $self = { %args }; |
258 | + return bless $self, $class; |
259 | +} |
260 | + |
261 | +sub generate_asc_stmt { |
262 | + my ( $self, %args ) = @_; |
263 | + my @required_args = qw(tbl_struct index); |
264 | + foreach my $arg ( @required_args ) { |
265 | + die "I need a $arg argument" unless defined $args{$arg}; |
266 | + } |
267 | + my ($tbl_struct, $index) = @args{@required_args}; |
268 | + my @cols = $args{cols} ? @{$args{cols}} : @{$tbl_struct->{cols}}; |
269 | + my $q = $self->{Quoter}; |
270 | + |
271 | + die "Index '$index' does not exist in table" |
272 | + unless exists $tbl_struct->{keys}->{$index}; |
273 | + PTDEBUG && _d('Will ascend index', $index); |
274 | + |
275 | + my @asc_cols = @{$tbl_struct->{keys}->{$index}->{cols}}; |
276 | + if ( $args{asc_first} ) { |
277 | + @asc_cols = $asc_cols[0]; |
278 | + PTDEBUG && _d('Ascending only first column'); |
279 | + } |
280 | + PTDEBUG && _d('Will ascend columns', join(', ', @asc_cols)); |
281 | + |
282 | + my @asc_slice; |
283 | + my %col_posn = do { my $i = 0; map { $_ => $i++ } @cols }; |
284 | + foreach my $col ( @asc_cols ) { |
285 | + if ( !exists $col_posn{$col} ) { |
286 | + push @cols, $col; |
287 | + $col_posn{$col} = $#cols; |
288 | + } |
289 | + push @asc_slice, $col_posn{$col}; |
290 | + } |
291 | + PTDEBUG && _d('Will ascend, in ordinal position:', join(', ', @asc_slice)); |
292 | + |
293 | + my $asc_stmt = { |
294 | + cols => \@cols, |
295 | + index => $index, |
296 | + where => '', |
297 | + slice => [], |
298 | + scols => [], |
299 | + }; |
300 | + |
301 | + if ( @asc_slice ) { |
302 | + my $cmp_where; |
303 | + foreach my $cmp ( qw(< <= >= >) ) { |
304 | + $cmp_where = $self->generate_cmp_where( |
305 | + type => $cmp, |
306 | + slice => \@asc_slice, |
307 | + cols => \@cols, |
308 | + quoter => $q, |
309 | + is_nullable => $tbl_struct->{is_nullable}, |
310 | + ); |
311 | + $asc_stmt->{boundaries}->{$cmp} = $cmp_where->{where}; |
312 | + } |
313 | + my $cmp = $args{asc_only} ? '>' : '>='; |
314 | + $asc_stmt->{where} = $asc_stmt->{boundaries}->{$cmp}; |
315 | + $asc_stmt->{slice} = $cmp_where->{slice}; |
316 | + $asc_stmt->{scols} = $cmp_where->{scols}; |
317 | + } |
318 | + |
319 | + return $asc_stmt; |
320 | +} |
321 | + |
322 | +sub generate_cmp_where { |
323 | + my ( $self, %args ) = @_; |
324 | + foreach my $arg ( qw(type slice cols is_nullable) ) { |
325 | + die "I need a $arg arg" unless defined $args{$arg}; |
326 | + } |
327 | + my @slice = @{$args{slice}}; |
328 | + my @cols = @{$args{cols}}; |
329 | + my $is_nullable = $args{is_nullable}; |
330 | + my $type = $args{type}; |
331 | + my $q = $self->{Quoter}; |
332 | + |
333 | + (my $cmp = $type) =~ s/=//; |
334 | + |
335 | + my @r_slice; # Resulting slice columns, by ordinal |
336 | + my @r_scols; # Ditto, by name |
337 | + |
338 | + my @clauses; |
339 | + foreach my $i ( 0 .. $#slice ) { |
340 | + my @clause; |
341 | + |
342 | + foreach my $j ( 0 .. $i - 1 ) { |
343 | + my $ord = $slice[$j]; |
344 | + my $col = $cols[$ord]; |
345 | + my $quo = $q->quote($col); |
346 | + if ( $is_nullable->{$col} ) { |
347 | + push @clause, "((? IS NULL AND $quo IS NULL) OR ($quo = ?))"; |
348 | + push @r_slice, $ord, $ord; |
349 | + push @r_scols, $col, $col; |
350 | + } |
351 | + else { |
352 | + push @clause, "$quo = ?"; |
353 | + push @r_slice, $ord; |
354 | + push @r_scols, $col; |
355 | + } |
356 | + } |
357 | + |
358 | + my $ord = $slice[$i]; |
359 | + my $col = $cols[$ord]; |
360 | + my $quo = $q->quote($col); |
361 | + my $end = $i == $#slice; # Last clause of the whole group. |
362 | + if ( $is_nullable->{$col} ) { |
363 | + if ( $type =~ m/=/ && $end ) { |
364 | + push @clause, "(? IS NULL OR $quo $type ?)"; |
365 | + } |
366 | + elsif ( $type =~ m/>/ ) { |
367 | + push @clause, "((? IS NULL AND $quo IS NOT NULL) OR ($quo $cmp ?))"; |
368 | + } |
369 | + else { # If $type =~ m/</ ) { |
370 | + push @clause, "((? IS NOT NULL AND $quo IS NULL) OR ($quo $cmp ?))"; |
371 | + } |
372 | + push @r_slice, $ord, $ord; |
373 | + push @r_scols, $col, $col; |
374 | + } |
375 | + else { |
376 | + push @r_slice, $ord; |
377 | + push @r_scols, $col; |
378 | + push @clause, ($type =~ m/=/ && $end ? "$quo $type ?" : "$quo $cmp ?"); |
379 | + } |
380 | + |
381 | + push @clauses, '(' . join(' AND ', @clause) . ')'; |
382 | + } |
383 | + my $result = '(' . join(' OR ', @clauses) . ')'; |
384 | + my $where = { |
385 | + slice => \@r_slice, |
386 | + scols => \@r_scols, |
387 | + where => $result, |
388 | + }; |
389 | + return $where; |
390 | +} |
391 | + |
392 | +sub generate_del_stmt { |
393 | + my ( $self, %args ) = @_; |
394 | + |
395 | + my $tbl = $args{tbl_struct}; |
396 | + my @cols = $args{cols} ? @{$args{cols}} : (); |
397 | + my $tp = $self->{TableParser}; |
398 | + my $q = $self->{Quoter}; |
399 | + |
400 | + my @del_cols; |
401 | + my @del_slice; |
402 | + |
403 | + my $index = $tp->find_best_index($tbl, $args{index}); |
404 | + die "Cannot find an ascendable index in table" unless $index; |
405 | + |
406 | + if ( $index ) { |
407 | + @del_cols = @{$tbl->{keys}->{$index}->{cols}}; |
408 | + } |
409 | + else { |
410 | + @del_cols = @{$tbl->{cols}}; |
411 | + } |
412 | + PTDEBUG && _d('Columns needed for DELETE:', join(', ', @del_cols)); |
413 | + |
414 | + my %col_posn = do { my $i = 0; map { $_ => $i++ } @cols }; |
415 | + foreach my $col ( @del_cols ) { |
416 | + if ( !exists $col_posn{$col} ) { |
417 | + push @cols, $col; |
418 | + $col_posn{$col} = $#cols; |
419 | + } |
420 | + push @del_slice, $col_posn{$col}; |
421 | + } |
422 | + PTDEBUG && _d('Ordinals needed for DELETE:', join(', ', @del_slice)); |
423 | + |
424 | + my $del_stmt = { |
425 | + cols => \@cols, |
426 | + index => $index, |
427 | + where => '', |
428 | + slice => [], |
429 | + scols => [], |
430 | + }; |
431 | + |
432 | + my @clauses; |
433 | + foreach my $i ( 0 .. $#del_slice ) { |
434 | + my $ord = $del_slice[$i]; |
435 | + my $col = $cols[$ord]; |
436 | + my $quo = $q->quote($col); |
437 | + if ( $tbl->{is_nullable}->{$col} ) { |
438 | + push @clauses, "((? IS NULL AND $quo IS NULL) OR ($quo = ?))"; |
439 | + push @{$del_stmt->{slice}}, $ord, $ord; |
440 | + push @{$del_stmt->{scols}}, $col, $col; |
441 | + } |
442 | + else { |
443 | + push @clauses, "$quo = ?"; |
444 | + push @{$del_stmt->{slice}}, $ord; |
445 | + push @{$del_stmt->{scols}}, $col; |
446 | + } |
447 | + } |
448 | + |
449 | + $del_stmt->{where} = '(' . join(' AND ', @clauses) . ')'; |
450 | + |
451 | + return $del_stmt; |
452 | +} |
453 | + |
454 | +sub generate_ins_stmt { |
455 | + my ( $self, %args ) = @_; |
456 | + foreach my $arg ( qw(ins_tbl sel_cols) ) { |
457 | + die "I need a $arg argument" unless $args{$arg}; |
458 | + } |
459 | + my $ins_tbl = $args{ins_tbl}; |
460 | + my @sel_cols = @{$args{sel_cols}}; |
461 | + |
462 | + die "You didn't specify any SELECT columns" unless @sel_cols; |
463 | + |
464 | + my @ins_cols; |
465 | + my @ins_slice; |
466 | + for my $i ( 0..$#sel_cols ) { |
467 | + next unless $ins_tbl->{is_col}->{$sel_cols[$i]}; |
468 | + push @ins_cols, $sel_cols[$i]; |
469 | + push @ins_slice, $i; |
470 | + } |
471 | + |
472 | + return { |
473 | + cols => \@ins_cols, |
474 | + slice => \@ins_slice, |
475 | + }; |
476 | } |
477 | |
478 | sub _d { |
479 | @@ -2036,7 +2037,7 @@ |
480 | 1; |
481 | } |
482 | # ########################################################################### |
483 | -# End Transformers package |
484 | +# End TableNibbler package |
485 | # ########################################################################### |
486 | |
487 | # ########################################################################### |
488 | @@ -2077,41 +2078,43 @@ |
489 | die "I need a tbl parameter" unless $tbl; |
490 | my $q = $self->{Quoter}; |
491 | |
492 | - my $sql = '/*!40101 SET @OLD_SQL_MODE := @@SQL_MODE, ' |
493 | - . q{@@SQL_MODE := REPLACE(REPLACE(@@SQL_MODE, 'ANSI_QUOTES', ''), ',,', ','), } |
494 | - . '@OLD_QUOTE := @@SQL_QUOTE_SHOW_CREATE, ' |
495 | - . '@@SQL_QUOTE_SHOW_CREATE := 1 */'; |
496 | - PTDEBUG && _d($sql); |
497 | - eval { $dbh->do($sql); }; |
498 | + my $new_sql_mode |
499 | + = '/*!40101 SET @OLD_SQL_MODE := @@SQL_MODE, ' |
500 | + . q{@@SQL_MODE := REPLACE(REPLACE(@@SQL_MODE, 'ANSI_QUOTES', ''), ',,', ','), } |
501 | + . '@OLD_QUOTE := @@SQL_QUOTE_SHOW_CREATE, ' |
502 | + . '@@SQL_QUOTE_SHOW_CREATE := 1 */'; |
503 | + |
504 | + my $old_sql_mode = '/*!40101 SET @@SQL_MODE := @OLD_SQL_MODE, ' |
505 | + . '@@SQL_QUOTE_SHOW_CREATE := @OLD_QUOTE */'; |
506 | + |
507 | + PTDEBUG && _d($new_sql_mode); |
508 | + eval { $dbh->do($new_sql_mode); }; |
509 | PTDEBUG && $EVAL_ERROR && _d($EVAL_ERROR); |
510 | |
511 | - $sql = 'USE ' . $q->quote($db); |
512 | - PTDEBUG && _d($dbh, $sql); |
513 | - $dbh->do($sql); |
514 | + my $use_sql = 'USE ' . $q->quote($db); |
515 | + PTDEBUG && _d($dbh, $use_sql); |
516 | + $dbh->do($use_sql); |
517 | |
518 | - $sql = "SHOW CREATE TABLE " . $q->quote($db, $tbl); |
519 | - PTDEBUG && _d($sql); |
520 | + my $show_sql = "SHOW CREATE TABLE " . $q->quote($db, $tbl); |
521 | + PTDEBUG && _d($show_sql); |
522 | my $href; |
523 | - eval { $href = $dbh->selectrow_hashref($sql); }; |
524 | + eval { $href = $dbh->selectrow_hashref($show_sql); }; |
525 | if ( $EVAL_ERROR ) { |
526 | PTDEBUG && _d($EVAL_ERROR); |
527 | + |
528 | + PTDEBUG && _d($old_sql_mode); |
529 | + $dbh->do($old_sql_mode); |
530 | + |
531 | return; |
532 | } |
533 | |
534 | - $sql = '/*!40101 SET @@SQL_MODE := @OLD_SQL_MODE, ' |
535 | - . '@@SQL_QUOTE_SHOW_CREATE := @OLD_QUOTE */'; |
536 | - PTDEBUG && _d($sql); |
537 | - $dbh->do($sql); |
538 | + PTDEBUG && _d($old_sql_mode); |
539 | + $dbh->do($old_sql_mode); |
540 | |
541 | - my ($key) = grep { m/create table/i } keys %$href; |
542 | - if ( $key ) { |
543 | - PTDEBUG && _d('This table is a base table'); |
544 | - $href->{$key} =~ s/\b[ ]{2,}/ /g; |
545 | - $href->{$key} .= "\n"; |
546 | - } |
547 | - else { |
548 | - PTDEBUG && _d('This table is a view'); |
549 | - ($key) = grep { m/create view/i } keys %$href; |
550 | + my ($key) = grep { m/create (?:table|view)/i } keys %$href; |
551 | + if ( !$key ) { |
552 | + die "Error: no 'Create Table' or 'Create View' in result set from " |
553 | + . "$show_sql: " . Dumper($href); |
554 | } |
555 | |
556 | return $href->{$key}; |
557 | @@ -2468,6 +2471,7 @@ |
558 | # ########################################################################### |
559 | |
560 | # ########################################################################### |
561 | +<<<<<<< TREE |
562 | # TableChunker package |
563 | # This package is a copy without comments from the original. The original |
564 | # with comments and its test file can be found in the Bazaar repository at, |
565 | @@ -3399,6 +3403,8 @@ |
566 | # ########################################################################### |
567 | |
568 | # ########################################################################### |
569 | +======= |
570 | +>>>>>>> MERGE-SOURCE |
571 | # Progress package |
572 | # This package is a copy without comments from the original. The original |
573 | # with comments and its test file can be found in the Bazaar repository at, |
574 | @@ -3547,241 +3553,6 @@ |
575 | # ########################################################################### |
576 | |
577 | # ########################################################################### |
578 | -# OSCCaptureSync package |
579 | -# This package is a copy without comments from the original. The original |
580 | -# with comments and its test file can be found in the Bazaar repository at, |
581 | -# lib/OSCCaptureSync.pm |
582 | -# t/lib/OSCCaptureSync.t |
583 | -# See https://launchpad.net/percona-toolkit for more information. |
584 | -# ########################################################################### |
585 | -{ |
586 | -package OSCCaptureSync; |
587 | - |
588 | -use strict; |
589 | -use warnings FATAL => 'all'; |
590 | -use English qw(-no_match_vars); |
591 | -use constant PTDEBUG => $ENV{PTDEBUG} || 0; |
592 | - |
593 | -sub new { |
594 | - my ( $class, %args ) = @_; |
595 | - my @required_args = qw(Quoter); |
596 | - foreach my $arg ( @required_args ) { |
597 | - die "I need a $arg argument" unless $args{$arg}; |
598 | - } |
599 | - |
600 | - my $self = { |
601 | - Quoter => $args{Quoter}, |
602 | - }; |
603 | - |
604 | - return bless $self, $class; |
605 | -} |
606 | - |
607 | -sub capture { |
608 | - my ( $self, %args ) = @_; |
609 | - my @required_args = qw(msg dbh db tbl tmp_tbl columns chunk_column); |
610 | - foreach my $arg ( @required_args ) { |
611 | - die "I need a $arg argument" unless $args{$arg}; |
612 | - } |
613 | - my ($msg, $dbh) = @args{@required_args}; |
614 | - |
615 | - my @triggers = $self->_make_triggers(%args); |
616 | - foreach my $sql ( @triggers ) { |
617 | - $msg->($sql); |
618 | - $dbh->do($sql) unless $args{print}; |
619 | - } |
620 | - |
621 | - return; |
622 | -} |
623 | - |
624 | -sub _make_triggers { |
625 | - my ( $self, %args ) = @_; |
626 | - my @required_args = qw(db tbl tmp_tbl chunk_column columns); |
627 | - foreach my $arg ( @required_args ) { |
628 | - die "I need a $arg argument" unless $args{$arg}; |
629 | - } |
630 | - my ($db, $tbl, $tmp_tbl, $chunk_column) = @args{@required_args}; |
631 | - my $q = $self->{Quoter}; |
632 | - |
633 | - $chunk_column = $q->quote($chunk_column); |
634 | - |
635 | - my $old_table = $q->quote($db, $tbl); |
636 | - my $new_table = $q->quote($db, $tmp_tbl); |
637 | - my $new_values = join(', ', map { "NEW.".$q->quote($_) } @{$args{columns}}); |
638 | - my $columns = join(', ', map { $q->quote($_) } @{$args{columns}}); |
639 | - |
640 | - my $delete_trigger = "CREATE TRIGGER mk_osc_del AFTER DELETE ON $old_table " |
641 | - . "FOR EACH ROW " |
642 | - . "DELETE IGNORE FROM $new_table " |
643 | - . "WHERE $new_table.$chunk_column = OLD.$chunk_column"; |
644 | - |
645 | - my $insert_trigger = "CREATE TRIGGER mk_osc_ins AFTER INSERT ON $old_table " |
646 | - . "FOR EACH ROW " |
647 | - . "REPLACE INTO $new_table ($columns) " |
648 | - . "VALUES($new_values)"; |
649 | - |
650 | - my $update_trigger = "CREATE TRIGGER mk_osc_upd AFTER UPDATE ON $old_table " |
651 | - . "FOR EACH ROW " |
652 | - . "REPLACE INTO $new_table ($columns) " |
653 | - . "VALUES ($new_values)"; |
654 | - |
655 | - return $delete_trigger, $update_trigger, $insert_trigger; |
656 | -} |
657 | - |
658 | -sub sync { |
659 | - my ( $self, %args ) = @_; |
660 | - my @required_args = qw(); |
661 | - foreach my $arg ( @required_args ) { |
662 | - die "I need a $arg argument" unless $args{$arg}; |
663 | - } |
664 | - return; |
665 | -} |
666 | - |
667 | -sub cleanup { |
668 | - my ( $self, %args ) = @_; |
669 | - my @required_args = qw(dbh db msg); |
670 | - foreach my $arg ( @required_args ) { |
671 | - die "I need a $arg argument" unless $args{$arg}; |
672 | - } |
673 | - my ($dbh, $db, $msg) = @args{@required_args}; |
674 | - my $q = $self->{Quoter}; |
675 | - |
676 | - foreach my $trigger ( qw(del ins upd) ) { |
677 | - my $sql = "DROP TRIGGER IF EXISTS " . $q->quote($db, "mk_osc_$trigger"); |
678 | - $msg->($sql); |
679 | - $dbh->do($sql) unless $args{print}; |
680 | - } |
681 | - |
682 | - return; |
683 | -} |
684 | - |
685 | -sub _d { |
686 | - my ($package, undef, $line) = caller 0; |
687 | - @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; } |
688 | - map { defined $_ ? $_ : 'undef' } |
689 | - @_; |
690 | - print STDERR "# $package:$line $PID ", join(' ', @_), "\n"; |
691 | -} |
692 | - |
693 | -1; |
694 | -} |
695 | -# ########################################################################### |
696 | -# End OSCCaptureSync package |
697 | -# ########################################################################### |
698 | - |
699 | -# ########################################################################### |
700 | -# CopyRowsInsertSelect package |
701 | -# This package is a copy without comments from the original. The original |
702 | -# with comments and its test file can be found in the Bazaar repository at, |
703 | -# lib/CopyRowsInsertSelect.pm |
704 | -# t/lib/CopyRowsInsertSelect.t |
705 | -# See https://launchpad.net/percona-toolkit for more information. |
706 | -# ########################################################################### |
707 | -{ |
708 | -package CopyRowsInsertSelect; |
709 | - |
710 | -use strict; |
711 | -use warnings FATAL => 'all'; |
712 | -use English qw(-no_match_vars); |
713 | -use constant PTDEBUG => $ENV{PTDEBUG} || 0; |
714 | - |
715 | -sub new { |
716 | - my ( $class, %args ) = @_; |
717 | - my @required_args = qw(Retry Quoter); |
718 | - foreach my $arg ( @required_args ) { |
719 | - die "I need a $arg argument" unless $args{$arg}; |
720 | - } |
721 | - |
722 | - my $self = { |
723 | - Retry => $args{Retry}, |
724 | - Quoter => $args{Quoter}, |
725 | - }; |
726 | - |
727 | - return bless $self, $class; |
728 | -} |
729 | - |
730 | -sub copy { |
731 | - my ( $self, %args ) = @_; |
732 | - my @required_args = qw(dbh msg from_table to_table chunks columns); |
733 | - foreach my $arg ( @required_args ) { |
734 | - die "I need a $arg argument" unless $args{$arg}; |
735 | - } |
736 | - my ($dbh, $msg, $from_table, $to_table, $chunks) = @args{@required_args}; |
737 | - my $q = $self->{Quoter}; |
738 | - my $pr = $args{Progress}; |
739 | - my $sleep = $args{sleep}; |
740 | - my $columns = join(', ', map { $q->quote($_) } @{$args{columns}}); |
741 | - my $n_chunks = @$chunks - 1; |
742 | - |
743 | - for my $chunkno ( 0..$n_chunks ) { |
744 | - if ( !$chunks->[$chunkno] ) { |
745 | - warn "Chunk number ", ($chunkno + 1), "is undefined"; |
746 | - next; |
747 | - } |
748 | - |
749 | - my $sql = "INSERT IGNORE INTO $to_table ($columns) " |
750 | - . "SELECT $columns FROM $from_table " |
751 | - . "WHERE ($chunks->[$chunkno])" |
752 | - . ($args{where} ? " AND ($args{where})" : "") |
753 | - . ($args{engine_flags} ? " $args{engine_flags}" : ""); |
754 | - |
755 | - if ( $args{print} ) { |
756 | - $msg->($sql); |
757 | - } |
758 | - else { |
759 | - PTDEBUG && _d($dbh, $sql); |
760 | - my $error; |
761 | - $self->{Retry}->retry( |
762 | - wait => sub { sleep 1; }, |
763 | - tries => 3, |
764 | - try => sub { |
765 | - $dbh->do($sql); |
766 | - return; |
767 | - }, |
768 | - fail => sub { |
769 | - my (%args) = @_; |
770 | - my $error = $args{error}; |
771 | - PTDEBUG && _d($error); |
772 | - if ( $error =~ m/Lock wait timeout exceeded/ ) { |
773 | - $msg->("Lock wait timeout exceeded; retrying $sql"); |
774 | - return 1; # call wait, call try |
775 | - } |
776 | - return 0; # call final_fail |
777 | - }, |
778 | - final_fail => sub { |
779 | - my (%args) = @_; |
780 | - die $args{error}; |
781 | - }, |
782 | - ); |
783 | - } |
784 | - |
785 | - $pr->update(sub { return $chunkno + 1; }) if $pr; |
786 | - |
787 | - $sleep->($chunkno + 1) if $sleep && $chunkno < $n_chunks; |
788 | - } |
789 | - |
790 | - return; |
791 | -} |
792 | - |
793 | -sub cleanup { |
794 | - my ( $self, %args ) = @_; |
795 | - return; |
796 | -} |
797 | - |
798 | -sub _d { |
799 | - my ($package, undef, $line) = caller 0; |
800 | - @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; } |
801 | - map { defined $_ ? $_ : 'undef' } |
802 | - @_; |
803 | - print STDERR "# $package:$line $PID ", join(' ', @_), "\n"; |
804 | -} |
805 | - |
806 | -1; |
807 | -} |
808 | -# ########################################################################### |
809 | -# End CopyRowsInsertSelect package |
810 | -# ########################################################################### |
811 | - |
812 | -# ########################################################################### |
813 | # Retry package |
814 | # This package is a copy without comments from the original. The original |
815 | # with comments and its test file can be found in the Bazaar repository at, |
816 | @@ -3860,6 +3631,2187 @@ |
817 | # ########################################################################### |
818 | |
819 | # ########################################################################### |
820 | +# Cxn package |
821 | +# This package is a copy without comments from the original. The original |
822 | +# with comments and its test file can be found in the Bazaar repository at, |
823 | +# lib/Cxn.pm |
824 | +# t/lib/Cxn.t |
825 | +# See https://launchpad.net/percona-toolkit for more information. |
826 | +# ########################################################################### |
827 | +{ |
828 | +package Cxn; |
829 | + |
830 | +use strict; |
831 | +use warnings FATAL => 'all'; |
832 | +use English qw(-no_match_vars); |
833 | +use constant PTDEBUG => $ENV{PTDEBUG} || 0; |
834 | + |
835 | +use constant PERCONA_TOOLKIT_TEST_USE_DSN_NAMES => $ENV{PERCONA_TOOLKIT_TEST_USE_DSN_NAMES} || 0; |
836 | + |
837 | +sub new { |
838 | + my ( $class, %args ) = @_; |
839 | + my @required_args = qw(DSNParser OptionParser); |
840 | + foreach my $arg ( @required_args ) { |
841 | + die "I need a $arg argument" unless $args{$arg}; |
842 | + }; |
843 | + my ($dp, $o) = @args{@required_args}; |
844 | + |
845 | + my $dsn_defaults = $dp->parse_options($o); |
846 | + my $prev_dsn = $args{prev_dsn}; |
847 | + my $dsn = $args{dsn}; |
848 | + if ( !$dsn ) { |
849 | + $args{dsn_string} ||= 'h=' . ($dsn_defaults->{h} || 'localhost'); |
850 | + |
851 | + $dsn = $dp->parse( |
852 | + $args{dsn_string}, $prev_dsn, $dsn_defaults); |
853 | + } |
854 | + elsif ( $prev_dsn ) { |
855 | + $dsn = $dp->copy($prev_dsn, $dsn); |
856 | + } |
857 | + |
858 | + my $self = { |
859 | + dsn => $dsn, |
860 | + dbh => $args{dbh}, |
861 | + dsn_name => $dp->as_string($dsn, [qw(h P S)]), |
862 | + hostname => '', |
863 | + set => $args{set}, |
864 | + dbh_set => 0, |
865 | + OptionParser => $o, |
866 | + DSNParser => $dp, |
867 | + }; |
868 | + |
869 | + return bless $self, $class; |
870 | +} |
871 | + |
872 | +sub connect { |
873 | + my ( $self ) = @_; |
874 | + my $dsn = $self->{dsn}; |
875 | + my $dp = $self->{DSNParser}; |
876 | + my $o = $self->{OptionParser}; |
877 | + |
878 | + my $dbh = $self->{dbh}; |
879 | + if ( !$dbh || !$dbh->ping() ) { |
880 | + if ( $o->get('ask-pass') && !$self->{asked_for_pass} ) { |
881 | + $dsn->{p} = OptionParser::prompt_noecho("Enter MySQL password: "); |
882 | + $self->{asked_for_pass} = 1; |
883 | + } |
884 | + $dbh = $dp->get_dbh($dp->get_cxn_params($dsn), { AutoCommit => 1 }); |
885 | + } |
886 | + PTDEBUG && _d($dbh, 'Connected dbh to', $self->{name}); |
887 | + |
888 | + return $self->set_dbh($dbh); |
889 | +} |
890 | + |
891 | +sub set_dbh { |
892 | + my ($self, $dbh) = @_; |
893 | + |
894 | + if ( $self->{dbh} && $self->{dbh} == $dbh && $self->{dbh_set} ) { |
895 | + PTDEBUG && _d($dbh, 'Already set dbh'); |
896 | + return $dbh; |
897 | + } |
898 | + |
899 | + PTDEBUG && _d($dbh, 'Setting dbh'); |
900 | + |
901 | + $dbh->{FetchHashKeyName} = 'NAME_lc'; |
902 | + |
903 | + my $sql = 'SELECT @@hostname, @@server_id'; |
904 | + PTDEBUG && _d($dbh, $sql); |
905 | + my ($hostname, $server_id) = $dbh->selectrow_array($sql); |
906 | + PTDEBUG && _d($dbh, 'hostname:', $hostname, $server_id); |
907 | + if ( $hostname ) { |
908 | + $self->{hostname} = $hostname; |
909 | + } |
910 | + |
911 | + if ( my $set = $self->{set}) { |
912 | + $set->($dbh); |
913 | + } |
914 | + |
915 | + $self->{dbh} = $dbh; |
916 | + $self->{dbh_set} = 1; |
917 | + return $dbh; |
918 | +} |
919 | + |
920 | +sub dbh { |
921 | + my ($self) = @_; |
922 | + return $self->{dbh}; |
923 | +} |
924 | + |
925 | +sub dsn { |
926 | + my ($self) = @_; |
927 | + return $self->{dsn}; |
928 | +} |
929 | + |
930 | +sub name { |
931 | + my ($self) = @_; |
932 | + return $self->{dsn_name} if PERCONA_TOOLKIT_TEST_USE_DSN_NAMES; |
933 | + return $self->{hostname} || $self->{dsn_name} || 'unknown host'; |
934 | +} |
935 | + |
936 | +sub DESTROY { |
937 | + my ($self) = @_; |
938 | + if ( $self->{dbh} ) { |
939 | + PTDEBUG && _d('Disconnecting dbh', $self->{dbh}, $self->{name}); |
940 | + $self->{dbh}->disconnect(); |
941 | + } |
942 | + return; |
943 | +} |
944 | + |
945 | +sub _d { |
946 | + my ($package, undef, $line) = caller 0; |
947 | + @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; } |
948 | + map { defined $_ ? $_ : 'undef' } |
949 | + @_; |
950 | + print STDERR "# $package:$line $PID ", join(' ', @_), "\n"; |
951 | +} |
952 | + |
953 | +1; |
954 | +} |
955 | +# ########################################################################### |
956 | +# End Cxn package |
957 | +# ########################################################################### |
958 | + |
959 | +# ########################################################################### |
960 | +# MasterSlave package |
961 | +# This package is a copy without comments from the original. The original |
962 | +# with comments and its test file can be found in the Bazaar repository at, |
963 | +# lib/MasterSlave.pm |
964 | +# t/lib/MasterSlave.t |
965 | +# See https://launchpad.net/percona-toolkit for more information. |
966 | +# ########################################################################### |
967 | +{ |
968 | +package MasterSlave; |
969 | + |
970 | +use strict; |
971 | +use warnings FATAL => 'all'; |
972 | +use English qw(-no_match_vars); |
973 | +use constant PTDEBUG => $ENV{PTDEBUG} || 0; |
974 | + |
975 | +sub new { |
976 | + my ( $class, %args ) = @_; |
977 | + my $self = { |
978 | + %args, |
979 | + replication_thread => {}, |
980 | + }; |
981 | + return bless $self, $class; |
982 | +} |
983 | + |
984 | +sub get_slaves { |
985 | + my ($self, %args) = @_; |
986 | + my @required_args = qw(make_cxn OptionParser DSNParser Quoter); |
987 | + foreach my $arg ( @required_args ) { |
988 | + die "I need a $arg argument" unless $args{$arg}; |
989 | + } |
990 | + my ($make_cxn, $o, $dp) = @args{@required_args}; |
991 | + |
992 | + my $slaves = []; |
993 | + my $method = $o->get('recursion-method'); |
994 | + PTDEBUG && _d('Slave recursion method:', $method); |
995 | + if ( !$method || $method =~ m/processlist|hosts/i ) { |
996 | + my @required_args = qw(dbh dsn); |
997 | + foreach my $arg ( @required_args ) { |
998 | + die "I need a $arg argument" unless $args{$arg}; |
999 | + } |
1000 | + my ($dbh, $dsn) = @args{@required_args}; |
1001 | + $self->recurse_to_slaves( |
1002 | + { dbh => $dbh, |
1003 | + dsn => $dsn, |
1004 | + dsn_parser => $dp, |
1005 | + recurse => $o->get('recurse'), |
1006 | + method => $o->get('recursion-method'), |
1007 | + callback => sub { |
1008 | + my ( $dsn, $dbh, $level, $parent ) = @_; |
1009 | + return unless $level; |
1010 | + PTDEBUG && _d('Found slave:', $dp->as_string($dsn)); |
1011 | + push @$slaves, $make_cxn->(dsn => $dsn, dbh => $dbh); |
1012 | + return; |
1013 | + }, |
1014 | + } |
1015 | + ); |
1016 | + } |
1017 | + elsif ( $method =~ m/^dsn=/i ) { |
1018 | + my ($dsn_table_dsn) = $method =~ m/^dsn=(.+)/i; |
1019 | + $slaves = $self->get_cxn_from_dsn_table( |
1020 | + %args, |
1021 | + dsn_table_dsn => $dsn_table_dsn, |
1022 | + ); |
1023 | + } |
1024 | + else { |
1025 | + die "Invalid --recursion-method: $method. Valid values are: " |
1026 | + . "dsn=DSN, hosts, or processlist.\n"; |
1027 | + } |
1028 | + |
1029 | + return $slaves; |
1030 | +} |
1031 | + |
1032 | +sub recurse_to_slaves { |
1033 | + my ( $self, $args, $level ) = @_; |
1034 | + $level ||= 0; |
1035 | + my $dp = $args->{dsn_parser}; |
1036 | + my $dsn = $args->{dsn}; |
1037 | + |
1038 | + my $dbh; |
1039 | + eval { |
1040 | + $dbh = $args->{dbh} || $dp->get_dbh( |
1041 | + $dp->get_cxn_params($dsn), { AutoCommit => 1 }); |
1042 | + PTDEBUG && _d('Connected to', $dp->as_string($dsn)); |
1043 | + }; |
1044 | + if ( $EVAL_ERROR ) { |
1045 | + print STDERR "Cannot connect to ", $dp->as_string($dsn), "\n" |
1046 | + or die "Cannot print: $OS_ERROR"; |
1047 | + return; |
1048 | + } |
1049 | + |
1050 | + my $sql = 'SELECT @@SERVER_ID'; |
1051 | + PTDEBUG && _d($sql); |
1052 | + my ($id) = $dbh->selectrow_array($sql); |
1053 | + PTDEBUG && _d('Working on server ID', $id); |
1054 | + my $master_thinks_i_am = $dsn->{server_id}; |
1055 | + if ( !defined $id |
1056 | + || ( defined $master_thinks_i_am && $master_thinks_i_am != $id ) |
1057 | + || $args->{server_ids_seen}->{$id}++ |
1058 | + ) { |
1059 | + PTDEBUG && _d('Server ID seen, or not what master said'); |
1060 | + if ( $args->{skip_callback} ) { |
1061 | + $args->{skip_callback}->($dsn, $dbh, $level, $args->{parent}); |
1062 | + } |
1063 | + return; |
1064 | + } |
1065 | + |
1066 | + $args->{callback}->($dsn, $dbh, $level, $args->{parent}); |
1067 | + |
1068 | + if ( !defined $args->{recurse} || $level < $args->{recurse} ) { |
1069 | + |
1070 | + my @slaves = |
1071 | + grep { !$_->{master_id} || $_->{master_id} == $id } # Only my slaves. |
1072 | + $self->find_slave_hosts($dp, $dbh, $dsn, $args->{method}); |
1073 | + |
1074 | + foreach my $slave ( @slaves ) { |
1075 | + PTDEBUG && _d('Recursing from', |
1076 | + $dp->as_string($dsn), 'to', $dp->as_string($slave)); |
1077 | + $self->recurse_to_slaves( |
1078 | + { %$args, dsn => $slave, dbh => undef, parent => $dsn }, $level + 1 ); |
1079 | + } |
1080 | + } |
1081 | +} |
1082 | + |
1083 | +sub find_slave_hosts { |
1084 | + my ( $self, $dsn_parser, $dbh, $dsn, $method ) = @_; |
1085 | + |
1086 | + my @methods = qw(processlist hosts); |
1087 | + if ( $method ) { |
1088 | + @methods = grep { $_ ne $method } @methods; |
1089 | + unshift @methods, $method; |
1090 | + } |
1091 | + else { |
1092 | + if ( ($dsn->{P} || 3306) != 3306 ) { |
1093 | + PTDEBUG && _d('Port number is non-standard; using only hosts method'); |
1094 | + @methods = qw(hosts); |
1095 | + } |
1096 | + } |
1097 | + PTDEBUG && _d('Looking for slaves on', $dsn_parser->as_string($dsn), |
1098 | + 'using methods', @methods); |
1099 | + |
1100 | + my @slaves; |
1101 | + METHOD: |
1102 | + foreach my $method ( @methods ) { |
1103 | + my $find_slaves = "_find_slaves_by_$method"; |
1104 | + PTDEBUG && _d('Finding slaves with', $find_slaves); |
1105 | + @slaves = $self->$find_slaves($dsn_parser, $dbh, $dsn); |
1106 | + last METHOD if @slaves; |
1107 | + } |
1108 | + |
1109 | + PTDEBUG && _d('Found', scalar(@slaves), 'slaves'); |
1110 | + return @slaves; |
1111 | +} |
1112 | + |
1113 | +sub _find_slaves_by_processlist { |
1114 | + my ( $self, $dsn_parser, $dbh, $dsn ) = @_; |
1115 | + |
1116 | + my @slaves = map { |
1117 | + my $slave = $dsn_parser->parse("h=$_", $dsn); |
1118 | + $slave->{source} = 'processlist'; |
1119 | + $slave; |
1120 | + } |
1121 | + grep { $_ } |
1122 | + map { |
1123 | + my ( $host ) = $_->{host} =~ m/^([^:]+):/; |
1124 | + if ( $host eq 'localhost' ) { |
1125 | + $host = '127.0.0.1'; # Replication never uses sockets. |
1126 | + } |
1127 | + $host; |
1128 | + } $self->get_connected_slaves($dbh); |
1129 | + |
1130 | + return @slaves; |
1131 | +} |
1132 | + |
1133 | +sub _find_slaves_by_hosts { |
1134 | + my ( $self, $dsn_parser, $dbh, $dsn ) = @_; |
1135 | + |
1136 | + my @slaves; |
1137 | + my $sql = 'SHOW SLAVE HOSTS'; |
1138 | + PTDEBUG && _d($dbh, $sql); |
1139 | + @slaves = @{$dbh->selectall_arrayref($sql, { Slice => {} })}; |
1140 | + |
1141 | + if ( @slaves ) { |
1142 | + PTDEBUG && _d('Found some SHOW SLAVE HOSTS info'); |
1143 | + @slaves = map { |
1144 | + my %hash; |
1145 | + @hash{ map { lc $_ } keys %$_ } = values %$_; |
1146 | + my $spec = "h=$hash{host},P=$hash{port}" |
1147 | + . ( $hash{user} ? ",u=$hash{user}" : '') |
1148 | + . ( $hash{password} ? ",p=$hash{password}" : ''); |
1149 | + my $dsn = $dsn_parser->parse($spec, $dsn); |
1150 | + $dsn->{server_id} = $hash{server_id}; |
1151 | + $dsn->{master_id} = $hash{master_id}; |
1152 | + $dsn->{source} = 'hosts'; |
1153 | + $dsn; |
1154 | + } @slaves; |
1155 | + } |
1156 | + |
1157 | + return @slaves; |
1158 | +} |
1159 | + |
1160 | +sub get_connected_slaves { |
1161 | + my ( $self, $dbh ) = @_; |
1162 | + |
1163 | + my $show = "SHOW GRANTS FOR "; |
1164 | + my $user = 'CURRENT_USER()'; |
1165 | + my $vp = $self->{VersionParser}; |
1166 | + if ( $vp && !$vp->version_ge($dbh, '4.1.2') ) { |
1167 | + $user = $dbh->selectrow_arrayref('SELECT USER()')->[0]; |
1168 | + $user =~ s/([^@]+)@(.+)/'$1'\@'$2'/; |
1169 | + } |
1170 | + my $sql = $show . $user; |
1171 | + PTDEBUG && _d($dbh, $sql); |
1172 | + |
1173 | + my $proc; |
1174 | + eval { |
1175 | + $proc = grep { |
1176 | + m/ALL PRIVILEGES.*?\*\.\*|PROCESS/ |
1177 | + } @{$dbh->selectcol_arrayref($sql)}; |
1178 | + }; |
1179 | + if ( $EVAL_ERROR ) { |
1180 | + |
1181 | + if ( $EVAL_ERROR =~ m/no such grant defined for user/ ) { |
1182 | + PTDEBUG && _d('Retrying SHOW GRANTS without host; error:', |
1183 | + $EVAL_ERROR); |
1184 | + ($user) = split('@', $user); |
1185 | + $sql = $show . $user; |
1186 | + PTDEBUG && _d($sql); |
1187 | + eval { |
1188 | + $proc = grep { |
1189 | + m/ALL PRIVILEGES.*?\*\.\*|PROCESS/ |
1190 | + } @{$dbh->selectcol_arrayref($sql)}; |
1191 | + }; |
1192 | + } |
1193 | + |
1194 | + die "Failed to $sql: $EVAL_ERROR" if $EVAL_ERROR; |
1195 | + } |
1196 | + if ( !$proc ) { |
1197 | + die "You do not have the PROCESS privilege"; |
1198 | + } |
1199 | + |
1200 | + $sql = 'SHOW PROCESSLIST'; |
1201 | + PTDEBUG && _d($dbh, $sql); |
1202 | + grep { $_->{command} =~ m/Binlog Dump/i } |
1203 | + map { # Lowercase the column names |
1204 | + my %hash; |
1205 | + @hash{ map { lc $_ } keys %$_ } = values %$_; |
1206 | + \%hash; |
1207 | + } |
1208 | + @{$dbh->selectall_arrayref($sql, { Slice => {} })}; |
1209 | +} |
1210 | + |
1211 | +sub is_master_of { |
1212 | + my ( $self, $master, $slave ) = @_; |
1213 | + my $master_status = $self->get_master_status($master) |
1214 | + or die "The server specified as a master is not a master"; |
1215 | + my $slave_status = $self->get_slave_status($slave) |
1216 | + or die "The server specified as a slave is not a slave"; |
1217 | + my @connected = $self->get_connected_slaves($master) |
1218 | + or die "The server specified as a master has no connected slaves"; |
1219 | + my (undef, $port) = $master->selectrow_array('SHOW VARIABLES LIKE "port"'); |
1220 | + |
1221 | + if ( $port != $slave_status->{master_port} ) { |
1222 | + die "The slave is connected to $slave_status->{master_port} " |
1223 | + . "but the master's port is $port"; |
1224 | + } |
1225 | + |
1226 | + if ( !grep { $slave_status->{master_user} eq $_->{user} } @connected ) { |
1227 | + die "I don't see any slave I/O thread connected with user " |
1228 | + . $slave_status->{master_user}; |
1229 | + } |
1230 | + |
1231 | + if ( ($slave_status->{slave_io_state} || '') |
1232 | + eq 'Waiting for master to send event' ) |
1233 | + { |
1234 | + my ( $master_log_name, $master_log_num ) |
1235 | + = $master_status->{file} =~ m/^(.*?)\.0*([1-9][0-9]*)$/; |
1236 | + my ( $slave_log_name, $slave_log_num ) |
1237 | + = $slave_status->{master_log_file} =~ m/^(.*?)\.0*([1-9][0-9]*)$/; |
1238 | + if ( $master_log_name ne $slave_log_name |
1239 | + || abs($master_log_num - $slave_log_num) > 1 ) |
1240 | + { |
1241 | + die "The slave thinks it is reading from " |
1242 | + . "$slave_status->{master_log_file}, but the " |
1243 | + . "master is writing to $master_status->{file}"; |
1244 | + } |
1245 | + } |
1246 | + return 1; |
1247 | +} |
1248 | + |
1249 | +sub get_master_dsn { |
1250 | + my ( $self, $dbh, $dsn, $dsn_parser ) = @_; |
1251 | + my $master = $self->get_slave_status($dbh) or return undef; |
1252 | + my $spec = "h=$master->{master_host},P=$master->{master_port}"; |
1253 | + return $dsn_parser->parse($spec, $dsn); |
1254 | +} |
1255 | + |
1256 | +sub get_slave_status { |
1257 | + my ( $self, $dbh ) = @_; |
1258 | + if ( !$self->{not_a_slave}->{$dbh} ) { |
1259 | + my $sth = $self->{sths}->{$dbh}->{SLAVE_STATUS} |
1260 | + ||= $dbh->prepare('SHOW SLAVE STATUS'); |
1261 | + PTDEBUG && _d($dbh, 'SHOW SLAVE STATUS'); |
1262 | + $sth->execute(); |
1263 | + my ($ss) = @{$sth->fetchall_arrayref({})}; |
1264 | + |
1265 | + if ( $ss && %$ss ) { |
1266 | + $ss = { map { lc($_) => $ss->{$_} } keys %$ss }; # lowercase the keys |
1267 | + return $ss; |
1268 | + } |
1269 | + |
1270 | + PTDEBUG && _d('This server returns nothing for SHOW SLAVE STATUS'); |
1271 | + $self->{not_a_slave}->{$dbh}++; |
1272 | + } |
1273 | +} |
1274 | + |
1275 | +sub get_master_status { |
1276 | + my ( $self, $dbh ) = @_; |
1277 | + |
1278 | + if ( $self->{not_a_master}->{$dbh} ) { |
1279 | + PTDEBUG && _d('Server on dbh', $dbh, 'is not a master'); |
1280 | + return; |
1281 | + } |
1282 | + |
1283 | + my $sth = $self->{sths}->{$dbh}->{MASTER_STATUS} |
1284 | + ||= $dbh->prepare('SHOW MASTER STATUS'); |
1285 | + PTDEBUG && _d($dbh, 'SHOW MASTER STATUS'); |
1286 | + $sth->execute(); |
1287 | + my ($ms) = @{$sth->fetchall_arrayref({})}; |
1288 | + PTDEBUG && _d( |
1289 | + $ms ? map { "$_=" . (defined $ms->{$_} ? $ms->{$_} : '') } keys %$ms |
1290 | + : ''); |
1291 | + |
1292 | + if ( !$ms || scalar keys %$ms < 2 ) { |
1293 | + PTDEBUG && _d('Server on dbh', $dbh, 'does not seem to be a master'); |
1294 | + $self->{not_a_master}->{$dbh}++; |
1295 | + } |
1296 | + |
1297 | + return { map { lc($_) => $ms->{$_} } keys %$ms }; # lowercase the keys |
1298 | +} |
1299 | + |
1300 | +sub wait_for_master { |
1301 | + my ( $self, %args ) = @_; |
1302 | + my @required_args = qw(master_status slave_dbh); |
1303 | + foreach my $arg ( @required_args ) { |
1304 | + die "I need a $arg argument" unless $args{$arg}; |
1305 | + } |
1306 | + my ($master_status, $slave_dbh) = @args{@required_args}; |
1307 | + my $timeout = $args{timeout} || 60; |
1308 | + |
1309 | + my $result; |
1310 | + my $waited; |
1311 | + if ( $master_status ) { |
1312 | + my $sql = "SELECT MASTER_POS_WAIT('$master_status->{file}', " |
1313 | + . "$master_status->{position}, $timeout)"; |
1314 | + PTDEBUG && _d($slave_dbh, $sql); |
1315 | + my $start = time; |
1316 | + ($result) = $slave_dbh->selectrow_array($sql); |
1317 | + |
1318 | + $waited = time - $start; |
1319 | + |
1320 | + PTDEBUG && _d('Result of waiting:', $result); |
1321 | + PTDEBUG && _d("Waited", $waited, "seconds"); |
1322 | + } |
1323 | + else { |
1324 | + PTDEBUG && _d('Not waiting: this server is not a master'); |
1325 | + } |
1326 | + |
1327 | + return { |
1328 | + result => $result, |
1329 | + waited => $waited, |
1330 | + }; |
1331 | +} |
1332 | + |
1333 | +sub stop_slave { |
1334 | + my ( $self, $dbh ) = @_; |
1335 | + my $sth = $self->{sths}->{$dbh}->{STOP_SLAVE} |
1336 | + ||= $dbh->prepare('STOP SLAVE'); |
1337 | + PTDEBUG && _d($dbh, $sth->{Statement}); |
1338 | + $sth->execute(); |
1339 | +} |
1340 | + |
1341 | +sub start_slave { |
1342 | + my ( $self, $dbh, $pos ) = @_; |
1343 | + if ( $pos ) { |
1344 | + my $sql = "START SLAVE UNTIL MASTER_LOG_FILE='$pos->{file}', " |
1345 | + . "MASTER_LOG_POS=$pos->{position}"; |
1346 | + PTDEBUG && _d($dbh, $sql); |
1347 | + $dbh->do($sql); |
1348 | + } |
1349 | + else { |
1350 | + my $sth = $self->{sths}->{$dbh}->{START_SLAVE} |
1351 | + ||= $dbh->prepare('START SLAVE'); |
1352 | + PTDEBUG && _d($dbh, $sth->{Statement}); |
1353 | + $sth->execute(); |
1354 | + } |
1355 | +} |
1356 | + |
1357 | +sub catchup_to_master { |
1358 | + my ( $self, $slave, $master, $timeout ) = @_; |
1359 | + $self->stop_slave($master); |
1360 | + $self->stop_slave($slave); |
1361 | + my $slave_status = $self->get_slave_status($slave); |
1362 | + my $slave_pos = $self->repl_posn($slave_status); |
1363 | + my $master_status = $self->get_master_status($master); |
1364 | + my $master_pos = $self->repl_posn($master_status); |
1365 | + PTDEBUG && _d('Master position:', $self->pos_to_string($master_pos), |
1366 | + 'Slave position:', $self->pos_to_string($slave_pos)); |
1367 | + |
1368 | + my $result; |
1369 | + if ( $self->pos_cmp($slave_pos, $master_pos) < 0 ) { |
1370 | + PTDEBUG && _d('Waiting for slave to catch up to master'); |
1371 | + $self->start_slave($slave, $master_pos); |
1372 | + |
1373 | + $result = $self->wait_for_master( |
1374 | + master_status => $master_status, |
1375 | + slave_dbh => $slave, |
1376 | + timeout => $timeout, |
1377 | + master_status => $master_status |
1378 | + ); |
1379 | + if ( !defined $result->{result} ) { |
1380 | + $slave_status = $self->get_slave_status($slave); |
1381 | + if ( !$self->slave_is_running($slave_status) ) { |
1382 | + PTDEBUG && _d('Master position:', |
1383 | + $self->pos_to_string($master_pos), |
1384 | + 'Slave position:', $self->pos_to_string($slave_pos)); |
1385 | + $slave_pos = $self->repl_posn($slave_status); |
1386 | + if ( $self->pos_cmp($slave_pos, $master_pos) != 0 ) { |
1387 | + die "MASTER_POS_WAIT() returned NULL but slave has not " |
1388 | + . "caught up to master"; |
1389 | + } |
1390 | + PTDEBUG && _d('Slave is caught up to master and stopped'); |
1391 | + } |
1392 | + else { |
1393 | + die "Slave has not caught up to master and it is still running"; |
1394 | + } |
1395 | + } |
1396 | + } |
1397 | + else { |
1398 | + PTDEBUG && _d("Slave is already caught up to master"); |
1399 | + } |
1400 | + |
1401 | + return $result; |
1402 | +} |
1403 | + |
1404 | +sub catchup_to_same_pos { |
1405 | + my ( $self, $s1_dbh, $s2_dbh ) = @_; |
1406 | + $self->stop_slave($s1_dbh); |
1407 | + $self->stop_slave($s2_dbh); |
1408 | + my $s1_status = $self->get_slave_status($s1_dbh); |
1409 | + my $s2_status = $self->get_slave_status($s2_dbh); |
1410 | + my $s1_pos = $self->repl_posn($s1_status); |
1411 | + my $s2_pos = $self->repl_posn($s2_status); |
1412 | + if ( $self->pos_cmp($s1_pos, $s2_pos) < 0 ) { |
1413 | + $self->start_slave($s1_dbh, $s2_pos); |
1414 | + } |
1415 | + elsif ( $self->pos_cmp($s2_pos, $s1_pos) < 0 ) { |
1416 | + $self->start_slave($s2_dbh, $s1_pos); |
1417 | + } |
1418 | + |
1419 | + $s1_status = $self->get_slave_status($s1_dbh); |
1420 | + $s2_status = $self->get_slave_status($s2_dbh); |
1421 | + $s1_pos = $self->repl_posn($s1_status); |
1422 | + $s2_pos = $self->repl_posn($s2_status); |
1423 | + |
1424 | + if ( $self->slave_is_running($s1_status) |
1425 | + || $self->slave_is_running($s2_status) |
1426 | + || $self->pos_cmp($s1_pos, $s2_pos) != 0) |
1427 | + { |
1428 | + die "The servers aren't both stopped at the same position"; |
1429 | + } |
1430 | + |
1431 | +} |
1432 | + |
1433 | +sub slave_is_running { |
1434 | + my ( $self, $slave_status ) = @_; |
1435 | + return ($slave_status->{slave_sql_running} || 'No') eq 'Yes'; |
1436 | +} |
1437 | + |
1438 | +sub has_slave_updates { |
1439 | + my ( $self, $dbh ) = @_; |
1440 | + my $sql = q{SHOW VARIABLES LIKE 'log_slave_updates'}; |
1441 | + PTDEBUG && _d($dbh, $sql); |
1442 | + my ($name, $value) = $dbh->selectrow_array($sql); |
1443 | + return $value && $value =~ m/^(1|ON)$/; |
1444 | +} |
1445 | + |
1446 | +sub repl_posn { |
1447 | + my ( $self, $status ) = @_; |
1448 | + if ( exists $status->{file} && exists $status->{position} ) { |
1449 | + return { |
1450 | + file => $status->{file}, |
1451 | + position => $status->{position}, |
1452 | + }; |
1453 | + } |
1454 | + else { |
1455 | + return { |
1456 | + file => $status->{relay_master_log_file}, |
1457 | + position => $status->{exec_master_log_pos}, |
1458 | + }; |
1459 | + } |
1460 | +} |
1461 | + |
1462 | +sub get_slave_lag { |
1463 | + my ( $self, $dbh ) = @_; |
1464 | + my $stat = $self->get_slave_status($dbh); |
1465 | + return unless $stat; # server is not a slave |
1466 | + return $stat->{seconds_behind_master}; |
1467 | +} |
1468 | + |
1469 | +sub pos_cmp { |
1470 | + my ( $self, $a, $b ) = @_; |
1471 | + return $self->pos_to_string($a) cmp $self->pos_to_string($b); |
1472 | +} |
1473 | + |
1474 | +sub short_host { |
1475 | + my ( $self, $dsn ) = @_; |
1476 | + my ($host, $port); |
1477 | + if ( $dsn->{master_host} ) { |
1478 | + $host = $dsn->{master_host}; |
1479 | + $port = $dsn->{master_port}; |
1480 | + } |
1481 | + else { |
1482 | + $host = $dsn->{h}; |
1483 | + $port = $dsn->{P}; |
1484 | + } |
1485 | + return ($host || '[default]') . ( ($port || 3306) == 3306 ? '' : ":$port" ); |
1486 | +} |
1487 | + |
1488 | +sub is_replication_thread { |
1489 | + my ( $self, $query, %args ) = @_; |
1490 | + return unless $query; |
1491 | + |
1492 | + my $type = lc($args{type} || 'all'); |
1493 | + die "Invalid type: $type" |
1494 | + unless $type =~ m/^binlog_dump|slave_io|slave_sql|all$/i; |
1495 | + |
1496 | + my $match = 0; |
1497 | + if ( $type =~ m/binlog_dump|all/i ) { |
1498 | + $match = 1 |
1499 | + if ($query->{Command} || $query->{command} || '') eq "Binlog Dump"; |
1500 | + } |
1501 | + if ( !$match ) { |
1502 | + if ( ($query->{User} || $query->{user} || '') eq "system user" ) { |
1503 | + PTDEBUG && _d("Slave replication thread"); |
1504 | + if ( $type ne 'all' ) { |
1505 | + my $state = $query->{State} || $query->{state} || ''; |
1506 | + |
1507 | + if ( $state =~ m/^init|end$/ ) { |
1508 | + PTDEBUG && _d("Special state:", $state); |
1509 | + $match = 1; |
1510 | + } |
1511 | + else { |
1512 | + my ($slave_sql) = $state =~ m/ |
1513 | + ^(Waiting\sfor\sthe\snext\sevent |
1514 | + |Reading\sevent\sfrom\sthe\srelay\slog |
1515 | + |Has\sread\sall\srelay\slog;\swaiting |
1516 | + |Making\stemp\sfile |
1517 | + |Waiting\sfor\sslave\smutex\son\sexit)/xi; |
1518 | + |
1519 | + $match = $type eq 'slave_sql' && $slave_sql ? 1 |
1520 | + : $type eq 'slave_io' && !$slave_sql ? 1 |
1521 | + : 0; |
1522 | + } |
1523 | + } |
1524 | + else { |
1525 | + $match = 1; |
1526 | + } |
1527 | + } |
1528 | + else { |
1529 | + PTDEBUG && _d('Not system user'); |
1530 | + } |
1531 | + |
1532 | + if ( !defined $args{check_known_ids} || $args{check_known_ids} ) { |
1533 | + my $id = $query->{Id} || $query->{id}; |
1534 | + if ( $match ) { |
1535 | + $self->{replication_thread}->{$id} = 1; |
1536 | + } |
1537 | + else { |
1538 | + if ( $self->{replication_thread}->{$id} ) { |
1539 | + PTDEBUG && _d("Thread ID is a known replication thread ID"); |
1540 | + $match = 1; |
1541 | + } |
1542 | + } |
1543 | + } |
1544 | + } |
1545 | + |
1546 | + PTDEBUG && _d('Matches', $type, 'replication thread:', |
1547 | + ($match ? 'yes' : 'no'), '; match:', $match); |
1548 | + |
1549 | + return $match; |
1550 | +} |
1551 | + |
1552 | + |
1553 | +sub get_replication_filters { |
1554 | + my ( $self, %args ) = @_; |
1555 | + my @required_args = qw(dbh); |
1556 | + foreach my $arg ( @required_args ) { |
1557 | + die "I need a $arg argument" unless $args{$arg}; |
1558 | + } |
1559 | + my ($dbh) = @args{@required_args}; |
1560 | + |
1561 | + my %filters = (); |
1562 | + |
1563 | + my $status = $self->get_master_status($dbh); |
1564 | + if ( $status ) { |
1565 | + map { $filters{$_} = $status->{$_} } |
1566 | + grep { defined $status->{$_} && $status->{$_} ne '' } |
1567 | + qw( |
1568 | + binlog_do_db |
1569 | + binlog_ignore_db |
1570 | + ); |
1571 | + } |
1572 | + |
1573 | + $status = $self->get_slave_status($dbh); |
1574 | + if ( $status ) { |
1575 | + map { $filters{$_} = $status->{$_} } |
1576 | + grep { defined $status->{$_} && $status->{$_} ne '' } |
1577 | + qw( |
1578 | + replicate_do_db |
1579 | + replicate_ignore_db |
1580 | + replicate_do_table |
1581 | + replicate_ignore_table |
1582 | + replicate_wild_do_table |
1583 | + replicate_wild_ignore_table |
1584 | + ); |
1585 | + |
1586 | + my $sql = "SHOW VARIABLES LIKE 'slave_skip_errors'"; |
1587 | + PTDEBUG && _d($dbh, $sql); |
1588 | + my $row = $dbh->selectrow_arrayref($sql); |
1589 | + $filters{slave_skip_errors} = $row->[1] if $row->[1] && $row->[1] ne 'OFF'; |
1590 | + } |
1591 | + |
1592 | + return \%filters; |
1593 | +} |
1594 | + |
1595 | + |
1596 | +sub pos_to_string { |
1597 | + my ( $self, $pos ) = @_; |
1598 | + my $fmt = '%s/%020d'; |
1599 | + return sprintf($fmt, @{$pos}{qw(file position)}); |
1600 | +} |
1601 | + |
1602 | +sub reset_known_replication_threads { |
1603 | + my ( $self ) = @_; |
1604 | + $self->{replication_thread} = {}; |
1605 | + return; |
1606 | +} |
1607 | + |
1608 | +sub get_cxn_from_dsn_table { |
1609 | + my ($self, %args) = @_; |
1610 | + my @required_args = qw(dsn_table_dsn make_cxn DSNParser Quoter); |
1611 | + foreach my $arg ( @required_args ) { |
1612 | + die "I need a $arg argument" unless $args{$arg}; |
1613 | + } |
1614 | + my ($dsn_table_dsn, $make_cxn, $dp, $q) = @args{@required_args}; |
1615 | + PTDEBUG && _d('DSN table DSN:', $dsn_table_dsn); |
1616 | + |
1617 | + my $dsn = $dp->parse($dsn_table_dsn); |
1618 | + my $dsn_table; |
1619 | + if ( $dsn->{D} && $dsn->{t} ) { |
1620 | + $dsn_table = $q->quote($dsn->{D}, $dsn->{t}); |
1621 | + } |
1622 | + elsif ( $dsn->{t} && $dsn->{t} =~ m/\./ ) { |
1623 | + $dsn_table = $q->quote($q->split_unquote($dsn->{t})); |
1624 | + } |
1625 | + else { |
1626 | + die "DSN table DSN does not specify a database (D) " |
1627 | + . "or a database-qualified table (t)"; |
1628 | + } |
1629 | + |
1630 | + my $dsn_tbl_cxn = $make_cxn->(dsn => $dsn); |
1631 | + my $dbh = $dsn_tbl_cxn->connect(); |
1632 | + my $sql = "SELECT dsn FROM $dsn_table ORDER BY id"; |
1633 | + PTDEBUG && _d($sql); |
1634 | + my $dsn_strings = $dbh->selectcol_arrayref($sql); |
1635 | + my @cxn; |
1636 | + if ( $dsn_strings ) { |
1637 | + foreach my $dsn_string ( @$dsn_strings ) { |
1638 | + PTDEBUG && _d('DSN from DSN table:', $dsn_string); |
1639 | + push @cxn, $make_cxn->(dsn_string => $dsn_string); |
1640 | + } |
1641 | + } |
1642 | + return \@cxn; |
1643 | +} |
1644 | + |
1645 | +sub _d { |
1646 | + my ($package, undef, $line) = caller 0; |
1647 | + @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; } |
1648 | + map { defined $_ ? $_ : 'undef' } |
1649 | + @_; |
1650 | + print STDERR "# $package:$line $PID ", join(' ', @_), "\n"; |
1651 | +} |
1652 | + |
1653 | +1; |
1654 | +} |
1655 | +# ########################################################################### |
1656 | +# End MasterSlave package |
1657 | +# ########################################################################### |
1658 | + |
1659 | +# ########################################################################### |
1660 | +# ReplicaLagWaiter package |
1661 | +# This package is a copy without comments from the original. The original |
1662 | +# with comments and its test file can be found in the Bazaar repository at, |
1663 | +# lib/ReplicaLagWaiter.pm |
1664 | +# t/lib/ReplicaLagWaiter.t |
1665 | +# See https://launchpad.net/percona-toolkit for more information. |
1666 | +# ########################################################################### |
1667 | +{ |
1668 | +package ReplicaLagWaiter; |
1669 | + |
1670 | +use strict; |
1671 | +use warnings FATAL => 'all'; |
1672 | +use English qw(-no_match_vars); |
1673 | +use constant PTDEBUG => $ENV{PTDEBUG} || 0; |
1674 | + |
1675 | +use Time::HiRes qw(sleep time); |
1676 | +use Data::Dumper; |
1677 | + |
1678 | +sub new { |
1679 | + my ( $class, %args ) = @_; |
1680 | + my @required_args = qw(oktorun get_lag sleep max_lag slaves); |
1681 | + foreach my $arg ( @required_args ) { |
1682 | + die "I need a $arg argument" unless defined $args{$arg}; |
1683 | + } |
1684 | + |
1685 | + my $self = { |
1686 | + %args, |
1687 | + }; |
1688 | + |
1689 | + return bless $self, $class; |
1690 | +} |
1691 | + |
1692 | +sub wait { |
1693 | + my ( $self, %args ) = @_; |
1694 | + my @required_args = qw(); |
1695 | + foreach my $arg ( @required_args ) { |
1696 | + die "I need a $arg argument" unless $args{$arg}; |
1697 | + } |
1698 | + my $pr = $args{Progress}; |
1699 | + |
1700 | + my $oktorun = $self->{oktorun}; |
1701 | + my $get_lag = $self->{get_lag}; |
1702 | + my $sleep = $self->{sleep}; |
1703 | + my $slaves = $self->{slaves}; |
1704 | + my $max_lag = $self->{max_lag}; |
1705 | + |
1706 | + my $worst; # most lagging slave |
1707 | + my $pr_callback; |
1708 | + my $pr_first_report; |
1709 | + if ( $pr ) { |
1710 | + $pr_callback = sub { |
1711 | + my ($fraction, $elapsed, $remaining, $eta, $completed) = @_; |
1712 | + my $dsn_name = $worst->{cxn}->name(); |
1713 | + if ( defined $worst->{lag} ) { |
1714 | + print STDERR "Replica lag is " . ($worst->{lag} || '?') |
1715 | + . " seconds on $dsn_name. Waiting.\n"; |
1716 | + } |
1717 | + else { |
1718 | + print STDERR "Replica $dsn_name is stopped. Waiting.\n"; |
1719 | + } |
1720 | + return; |
1721 | + }; |
1722 | + $pr->set_callback($pr_callback); |
1723 | + |
1724 | + $pr_first_report = sub { |
1725 | + my $dsn_name = $worst->{cxn}->name(); |
1726 | + if ( !defined $worst->{lag} ) { |
1727 | + print STDERR "Replica $dsn_name is stopped. Waiting.\n"; |
1728 | + } |
1729 | + return; |
1730 | + }; |
1731 | + } |
1732 | + |
1733 | + my @lagged_slaves = map { {cxn=>$_, lag=>undef} } @$slaves; |
1734 | + while ( $oktorun->() && @lagged_slaves ) { |
1735 | + PTDEBUG && _d('Checking slave lag'); |
1736 | + for my $i ( 0..$#lagged_slaves ) { |
1737 | + my $lag = $get_lag->($lagged_slaves[$i]->{cxn}); |
1738 | + PTDEBUG && _d($lagged_slaves[$i]->{cxn}->name(), |
1739 | + 'slave lag:', $lag); |
1740 | + if ( !defined $lag || $lag > $max_lag ) { |
1741 | + $lagged_slaves[$i]->{lag} = $lag; |
1742 | + } |
1743 | + else { |
1744 | + delete $lagged_slaves[$i]; |
1745 | + } |
1746 | + } |
1747 | + |
1748 | + @lagged_slaves = grep { defined $_ } @lagged_slaves; |
1749 | + if ( @lagged_slaves ) { |
1750 | + @lagged_slaves = reverse sort { |
1751 | + defined $a->{lag} && defined $b->{lag} ? $a->{lag} <=> $b->{lag} |
1752 | + : defined $a->{lag} ? -1 |
1753 | + : 1; |
1754 | + } @lagged_slaves; |
1755 | + $worst = $lagged_slaves[0]; |
1756 | + PTDEBUG && _d(scalar @lagged_slaves, 'slaves are lagging, worst:', |
1757 | + $worst->{lag}, 'on', Dumper($worst->{cxn}->dsn())); |
1758 | + |
1759 | + if ( $pr ) { |
1760 | + $pr->update( |
1761 | + sub { return 0; }, |
1762 | + first_report => $pr_first_report, |
1763 | + ); |
1764 | + } |
1765 | + |
1766 | + PTDEBUG && _d('Calling sleep callback'); |
1767 | + $sleep->($worst->{cxn}, $worst->{lag}); |
1768 | + } |
1769 | + } |
1770 | + |
1771 | + PTDEBUG && _d('All slaves caught up'); |
1772 | + return; |
1773 | +} |
1774 | + |
1775 | +sub _d { |
1776 | + my ($package, undef, $line) = caller 0; |
1777 | + @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; } |
1778 | + map { defined $_ ? $_ : 'undef' } |
1779 | + @_; |
1780 | + print STDERR "# $package:$line $PID ", join(' ', @_), "\n"; |
1781 | +} |
1782 | + |
1783 | +1; |
1784 | +} |
1785 | +# ########################################################################### |
1786 | +# End ReplicaLagWaiter package |
1787 | +# ########################################################################### |
1788 | + |
1789 | +# ########################################################################### |
1790 | +# MySQLStatusWaiter package |
1791 | +# This package is a copy without comments from the original. The original |
1792 | +# with comments and its test file can be found in the Bazaar repository at, |
1793 | +# lib/MySQLStatusWaiter.pm |
1794 | +# t/lib/MySQLStatusWaiter.t |
1795 | +# See https://launchpad.net/percona-toolkit for more information. |
1796 | +# ########################################################################### |
1797 | +{ |
1798 | +package MySQLStatusWaiter; |
1799 | + |
1800 | +use strict; |
1801 | +use warnings FATAL => 'all'; |
1802 | +use English qw(-no_match_vars); |
1803 | +use constant PTDEBUG => $ENV{PTDEBUG} || 0; |
1804 | + |
1805 | +sub new { |
1806 | + my ( $class, %args ) = @_; |
1807 | + my @required_args = qw(max_spec get_status sleep oktorun); |
1808 | + foreach my $arg ( @required_args ) { |
1809 | + die "I need a $arg argument" unless defined $args{$arg}; |
1810 | + } |
1811 | + |
1812 | + PTDEBUG && _d('Parsing spec for max thresholds'); |
1813 | + my $max_val_for = _parse_spec( |
1814 | + spec => $args{max_spec}, |
1815 | + get_status => $args{get_status}, |
1816 | + threshold_factor => 0.2, # +20% |
1817 | + ); |
1818 | + |
1819 | + PTDEBUG && _d('Parsing spec for critical thresholds'); |
1820 | + my $critical_val_for = _parse_spec( |
1821 | + spec => $args{critical_spec} || [], |
1822 | + get_status => $args{get_status}, |
1823 | + threshold_factor => 1.0, # double (x2; +100%) |
1824 | + ); |
1825 | + |
1826 | + my $self = { |
1827 | + get_status => $args{get_status}, |
1828 | + sleep => $args{sleep}, |
1829 | + oktorun => $args{oktorun}, |
1830 | + max_val_for => $max_val_for, |
1831 | + critical_val_for => $critical_val_for, |
1832 | + }; |
1833 | + |
1834 | + return bless $self, $class; |
1835 | +} |
1836 | + |
1837 | +sub _parse_spec { |
1838 | + my ( %args ) = @_; |
1839 | + my @required_args = qw(spec get_status); |
1840 | + foreach my $arg ( @required_args ) { |
1841 | + die "I need a $arg argument" unless defined $args{$arg}; |
1842 | + } |
1843 | + my ($spec, $get_status) = @args{@required_args}; |
1844 | + |
1845 | + return unless $spec && scalar @$spec; |
1846 | + my $threshold_factor = $args{threshold_factor} || 0.20; |
1847 | + |
1848 | + my %max_val_for; |
1849 | + foreach my $var_val ( @$spec ) { |
1850 | + my ($var, $val) = split /[:=]/, $var_val; |
1851 | + die "Invalid spec: $var_val" unless $var; |
1852 | + if ( !$val ) { |
1853 | + my $init_val = $get_status->($var); |
1854 | + PTDEBUG && _d('Initial', $var, 'value:', $init_val); |
1855 | + $val = int(($init_val * $threshold_factor) + $init_val); |
1856 | + } |
1857 | + PTDEBUG && _d('Wait if', $var, '>=', $val); |
1858 | + $max_val_for{$var} = $val; |
1859 | + } |
1860 | + |
1861 | + return \%max_val_for; |
1862 | +} |
1863 | + |
1864 | +sub max_values { |
1865 | + my ($self) = @_; |
1866 | + return $self->{max_val_for}; |
1867 | +} |
1868 | + |
1869 | +sub critical_values { |
1870 | + my ($self) = @_; |
1871 | + return $self->{critical_val_for}; |
1872 | +} |
1873 | + |
1874 | +sub wait { |
1875 | + my ( $self, %args ) = @_; |
1876 | + |
1877 | + return unless $self->{max_val_for}; |
1878 | + |
1879 | + my $pr = $args{Progress}; # optional |
1880 | + |
1881 | + my $oktorun = $self->{oktorun}; |
1882 | + my $get_status = $self->{get_status}; |
1883 | + my $sleep = $self->{sleep}; |
1884 | + |
1885 | + my %vals_too_high = %{$self->{max_val_for}}; |
1886 | + my $pr_callback; |
1887 | + if ( $pr ) { |
1888 | + $pr_callback = sub { |
1889 | + print STDERR "Pausing because " |
1890 | + . join(', ', |
1891 | + map { |
1892 | + "$_=" |
1893 | + . (defined $vals_too_high{$_} ? $vals_too_high{$_} |
1894 | + : 'unknown') |
1895 | + } sort keys %vals_too_high |
1896 | + ) |
1897 | + . ".\n"; |
1898 | + return; |
1899 | + }; |
1900 | + $pr->set_callback($pr_callback); |
1901 | + } |
1902 | + |
1903 | + while ( $oktorun->() ) { |
1904 | + PTDEBUG && _d('Checking status variables'); |
1905 | + foreach my $var ( sort keys %vals_too_high ) { |
1906 | + my $val = $get_status->($var); |
1907 | + PTDEBUG && _d($var, '=', $val); |
1908 | + if ( $val |
1909 | + && exists $self->{critical_val_for}->{$var} |
1910 | + && $val >= $self->{critical_val_for}->{$var} ) { |
1911 | + die "$var=$val exceeds its critical threshold " |
1912 | + . "$self->{critical_val_for}->{$var}\n"; |
1913 | + } |
1914 | + if ( !$val || $val >= $self->{max_val_for}->{$var} ) { |
1915 | + $vals_too_high{$var} = $val; |
1916 | + } |
1917 | + else { |
1918 | + delete $vals_too_high{$var}; |
1919 | + } |
1920 | + } |
1921 | + |
1922 | + last unless scalar keys %vals_too_high; |
1923 | + |
1924 | + PTDEBUG && _d(scalar keys %vals_too_high, 'values are too high:', |
1925 | + %vals_too_high); |
1926 | + if ( $pr ) { |
1927 | + $pr->update(sub { return 0; }); |
1928 | + } |
1929 | + PTDEBUG && _d('Calling sleep callback'); |
1930 | + $sleep->(); |
1931 | + %vals_too_high = %{$self->{max_val_for}}; # recheck all vars |
1932 | + } |
1933 | + |
1934 | + PTDEBUG && _d('All var vals are low enough'); |
1935 | + return; |
1936 | +} |
1937 | + |
1938 | +sub _d { |
1939 | + my ($package, undef, $line) = caller 0; |
1940 | + @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; } |
1941 | + map { defined $_ ? $_ : 'undef' } |
1942 | + @_; |
1943 | + print STDERR "# $package:$line $PID ", join(' ', @_), "\n"; |
1944 | +} |
1945 | + |
1946 | +1; |
1947 | +} |
1948 | +# ########################################################################### |
1949 | +# End MySQLStatusWaiter package |
1950 | +# ########################################################################### |
1951 | + |
1952 | +# ########################################################################### |
1953 | +# WeightedAvgRate package |
1954 | +# This package is a copy without comments from the original. The original |
1955 | +# with comments and its test file can be found in the Bazaar repository at, |
1956 | +# lib/WeightedAvgRate.pm |
1957 | +# t/lib/WeightedAvgRate.t |
1958 | +# See https://launchpad.net/percona-toolkit for more information. |
1959 | +# ########################################################################### |
1960 | +{ |
1961 | +package WeightedAvgRate; |
1962 | + |
1963 | +use strict; |
1964 | +use warnings FATAL => 'all'; |
1965 | +use English qw(-no_match_vars); |
1966 | +use constant PTDEBUG => $ENV{PTDEBUG} || 0; |
1967 | + |
1968 | +sub new { |
1969 | + my ( $class, %args ) = @_; |
1970 | + my @required_args = qw(target_t); |
1971 | + foreach my $arg ( @required_args ) { |
1972 | + die "I need a $arg argument" unless defined $args{$arg}; |
1973 | + } |
1974 | + |
1975 | + my $self = { |
1976 | + %args, |
1977 | + avg_n => 0, |
1978 | + avg_t => 0, |
1979 | + weight => $args{weight} || 0.75, |
1980 | + }; |
1981 | + |
1982 | + return bless $self, $class; |
1983 | +} |
1984 | + |
1985 | +sub update { |
1986 | + my ($self, $n, $t) = @_; |
1987 | + PTDEBUG && _d('Master op time:', $n, 'n /', $t, 's'); |
1988 | + |
1989 | + if ( $self->{avg_n} && $self->{avg_t} ) { |
1990 | + $self->{avg_n} = ($self->{avg_n} * $self->{weight}) + $n; |
1991 | + $self->{avg_t} = ($self->{avg_t} * $self->{weight}) + $t; |
1992 | + $self->{avg_rate} = $self->{avg_n} / $self->{avg_t}; |
1993 | + PTDEBUG && _d('Weighted avg rate:', $self->{avg_rate}, 'n/s'); |
1994 | + } |
1995 | + else { |
1996 | + $self->{avg_n} = $n; |
1997 | + $self->{avg_t} = $t; |
1998 | + $self->{avg_rate} = $self->{avg_n} / $self->{avg_t}; |
1999 | + PTDEBUG && _d('Initial avg rate:', $self->{avg_rate}, 'n/s'); |
2000 | + } |
2001 | + |
2002 | + my $new_n = int($self->{avg_rate} * $self->{target_t}); |
2003 | + PTDEBUG && _d('Adjust n to', $new_n); |
2004 | + return $new_n; |
2005 | +} |
2006 | + |
2007 | +sub _d { |
2008 | + my ($package, undef, $line) = caller 0; |
2009 | + @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; } |
2010 | + map { defined $_ ? $_ : 'undef' } |
2011 | + @_; |
2012 | + print STDERR "# $package:$line $PID ", join(' ', @_), "\n"; |
2013 | +} |
2014 | + |
2015 | +1; |
2016 | +} |
2017 | +# ########################################################################### |
2018 | +# End WeightedAvgRate package |
2019 | +# ########################################################################### |
2020 | + |
2021 | +# ########################################################################### |
2022 | +# NibbleIterator package |
2023 | +# This package is a copy without comments from the original. The original |
2024 | +# with comments and its test file can be found in the Bazaar repository at, |
2025 | +# lib/NibbleIterator.pm |
2026 | +# t/lib/NibbleIterator.t |
2027 | +# See https://launchpad.net/percona-toolkit for more information. |
2028 | +# ########################################################################### |
2029 | +{ |
2030 | +package NibbleIterator; |
2031 | + |
2032 | +use strict; |
2033 | +use warnings FATAL => 'all'; |
2034 | +use English qw(-no_match_vars); |
2035 | +use constant PTDEBUG => $ENV{PTDEBUG} || 0; |
2036 | + |
2037 | +use Data::Dumper; |
2038 | +$Data::Dumper::Indent = 1; |
2039 | +$Data::Dumper::Sortkeys = 1; |
2040 | +$Data::Dumper::Quotekeys = 0; |
2041 | + |
2042 | +sub new { |
2043 | + my ( $class, %args ) = @_; |
2044 | + my @required_args = qw(Cxn tbl chunk_size OptionParser Quoter TableNibbler TableParser); |
2045 | + foreach my $arg ( @required_args ) { |
2046 | + die "I need a $arg argument" unless $args{$arg}; |
2047 | + } |
2048 | + my ($cxn, $tbl, $chunk_size, $o, $q) = @args{@required_args}; |
2049 | + |
2050 | + my $nibble_params = can_nibble(%args); |
2051 | + |
2052 | + my %comments = ( |
2053 | + bite => "bite table", |
2054 | + nibble => "nibble table", |
2055 | + ); |
2056 | + if ( $args{comments} ) { |
2057 | + map { $comments{$_} = $args{comments}->{$_} } |
2058 | + grep { defined $args{comments}->{$_} } |
2059 | + keys %{$args{comments}}; |
2060 | + } |
2061 | + |
2062 | + my $where = $o->has('where') ? $o->get('where') : ''; |
2063 | + my $tbl_struct = $tbl->{tbl_struct}; |
2064 | + my $ignore_col = $o->has('ignore-columns') |
2065 | + ? ($o->get('ignore-columns') || {}) |
2066 | + : {}; |
2067 | + my $all_cols = $o->has('columns') |
2068 | + ? ($o->get('columns') || $tbl_struct->{cols}) |
2069 | + : $tbl_struct->{cols}; |
2070 | + my @cols = grep { !$ignore_col->{$_} } @$all_cols; |
2071 | + my $self; |
2072 | + if ( $nibble_params->{one_nibble} ) { |
2073 | + my $nibble_sql |
2074 | + = ($args{dml} ? "$args{dml} " : "SELECT ") |
2075 | + . ($args{select} ? $args{select} |
2076 | + : join(', ', map { $q->quote($_) } @cols)) |
2077 | + . " FROM $tbl->{name}" |
2078 | + . ($where ? " WHERE $where" : '') |
2079 | + . " /*$comments{bite}*/"; |
2080 | + PTDEBUG && _d('One nibble statement:', $nibble_sql); |
2081 | + |
2082 | + my $explain_nibble_sql |
2083 | + = "EXPLAIN SELECT " |
2084 | + . ($args{select} ? $args{select} |
2085 | + : join(', ', map { $q->quote($_) } @cols)) |
2086 | + . " FROM $tbl->{name}" |
2087 | + . ($where ? " WHERE $where" : '') |
2088 | + . " /*explain $comments{bite}*/"; |
2089 | + PTDEBUG && _d('Explain one nibble statement:', $explain_nibble_sql); |
2090 | + |
2091 | + $self = { |
2092 | + %args, |
2093 | + one_nibble => 1, |
2094 | + limit => 0, |
2095 | + nibble_sql => $nibble_sql, |
2096 | + explain_nibble_sql => $explain_nibble_sql, |
2097 | + }; |
2098 | + } |
2099 | + else { |
2100 | + my $index = $nibble_params->{index}; # brevity |
2101 | + my $index_cols = $tbl->{tbl_struct}->{keys}->{$index}->{cols}; |
2102 | + |
2103 | + my $asc = $args{TableNibbler}->generate_asc_stmt( |
2104 | + %args, |
2105 | + tbl_struct => $tbl->{tbl_struct}, |
2106 | + index => $index, |
2107 | + cols => \@cols, |
2108 | + asc_only => 1, |
2109 | + ); |
2110 | + PTDEBUG && _d('Ascend params:', Dumper($asc)); |
2111 | + |
2112 | + my $from = "$tbl->{name} FORCE INDEX(`$index`)"; |
2113 | + my $order_by = join(', ', map {$q->quote($_)} @{$index_cols}); |
2114 | + |
2115 | + my $first_lb_sql |
2116 | + = "SELECT /*!40001 SQL_NO_CACHE */ " |
2117 | + . join(', ', map { $q->quote($_) } @{$asc->{scols}}) |
2118 | + . " FROM $from" |
2119 | + . ($where ? " WHERE $where" : '') |
2120 | + . " ORDER BY $order_by" |
2121 | + . " LIMIT 1" |
2122 | + . " /*first lower boundary*/"; |
2123 | + PTDEBUG && _d('First lower boundary statement:', $first_lb_sql); |
2124 | + |
2125 | + my $resume_lb_sql; |
2126 | + if ( $args{resume} ) { |
2127 | + $resume_lb_sql |
2128 | + = "SELECT /*!40001 SQL_NO_CACHE */ " |
2129 | + . join(', ', map { $q->quote($_) } @{$asc->{scols}}) |
2130 | + . " FROM $from" |
2131 | + . " WHERE " . $asc->{boundaries}->{'>'} |
2132 | + . ($where ? " AND ($where)" : '') |
2133 | + . " ORDER BY $order_by" |
2134 | + . " LIMIT 1" |
2135 | + . " /*resume lower boundary*/"; |
2136 | + PTDEBUG && _d('Resume lower boundary statement:', $resume_lb_sql); |
2137 | + } |
2138 | + |
2139 | + my $last_ub_sql |
2140 | + = "SELECT /*!40001 SQL_NO_CACHE */ " |
2141 | + . join(', ', map { $q->quote($_) } @{$asc->{scols}}) |
2142 | + . " FROM $from" |
2143 | + . ($where ? " WHERE $where" : '') |
2144 | + . " ORDER BY " |
2145 | + . join(' DESC, ', map {$q->quote($_)} @{$index_cols}) . ' DESC' |
2146 | + . " LIMIT 1" |
2147 | + . " /*last upper boundary*/"; |
2148 | + PTDEBUG && _d('Last upper boundary statement:', $last_ub_sql); |
2149 | + |
2150 | + my $ub_sql |
2151 | + = "SELECT /*!40001 SQL_NO_CACHE */ " |
2152 | + . join(', ', map { $q->quote($_) } @{$asc->{scols}}) |
2153 | + . " FROM $from" |
2154 | + . " WHERE " . $asc->{boundaries}->{'>='} |
2155 | + . ($where ? " AND ($where)" : '') |
2156 | + . " ORDER BY $order_by" |
2157 | + . " LIMIT ?, 2" |
2158 | + . " /*next chunk boundary*/"; |
2159 | + PTDEBUG && _d('Upper boundary statement:', $ub_sql); |
2160 | + |
2161 | + my $nibble_sql |
2162 | + = ($args{dml} ? "$args{dml} " : "SELECT ") |
2163 | + . ($args{select} ? $args{select} |
2164 | + : join(', ', map { $q->quote($_) } @{$asc->{cols}})) |
2165 | + . " FROM $from" |
2166 | + . " WHERE " . $asc->{boundaries}->{'>='} # lower boundary |
2167 | + . " AND " . $asc->{boundaries}->{'<='} # upper boundary |
2168 | + . ($where ? " AND ($where)" : '') |
2169 | + . ($args{order_by} ? " ORDER BY $order_by" : "") |
2170 | + . " /*$comments{nibble}*/"; |
2171 | + PTDEBUG && _d('Nibble statement:', $nibble_sql); |
2172 | + |
2173 | + my $explain_nibble_sql |
2174 | + = "EXPLAIN SELECT " |
2175 | + . ($args{select} ? $args{select} |
2176 | + : join(', ', map { $q->quote($_) } @{$asc->{cols}})) |
2177 | + . " FROM $from" |
2178 | + . " WHERE " . $asc->{boundaries}->{'>='} # lower boundary |
2179 | + . " AND " . $asc->{boundaries}->{'<='} # upper boundary |
2180 | + . ($where ? " AND ($where)" : '') |
2181 | + . ($args{order_by} ? " ORDER BY $order_by" : "") |
2182 | + . " /*explain $comments{nibble}*/"; |
2183 | + PTDEBUG && _d('Explain nibble statement:', $explain_nibble_sql); |
2184 | + |
2185 | + my $limit = $chunk_size - 1; |
2186 | + PTDEBUG && _d('Initial chunk size (LIMIT):', $limit); |
2187 | + |
2188 | + $self = { |
2189 | + %args, |
2190 | + index => $index, |
2191 | + limit => $limit, |
2192 | + first_lb_sql => $first_lb_sql, |
2193 | + last_ub_sql => $last_ub_sql, |
2194 | + ub_sql => $ub_sql, |
2195 | + nibble_sql => $nibble_sql, |
2196 | + explain_ub_sql => "EXPLAIN $ub_sql", |
2197 | + explain_nibble_sql => $explain_nibble_sql, |
2198 | + resume_lb_sql => $resume_lb_sql, |
2199 | + sql => { |
2200 | + columns => $asc->{scols}, |
2201 | + from => $from, |
2202 | + where => $where, |
2203 | + boundaries => $asc->{boundaries}, |
2204 | + order_by => $order_by, |
2205 | + }, |
2206 | + }; |
2207 | + } |
2208 | + |
2209 | + $self->{row_est} = $nibble_params->{row_est}, |
2210 | + $self->{nibbleno} = 0; |
2211 | + $self->{have_rows} = 0; |
2212 | + $self->{rowno} = 0; |
2213 | + $self->{oktonibble} = 1; |
2214 | + |
2215 | + return bless $self, $class; |
2216 | +} |
2217 | + |
2218 | +sub next { |
2219 | + my ($self) = @_; |
2220 | + |
2221 | + if ( !$self->{oktonibble} ) { |
2222 | + PTDEBUG && _d('Not ok to nibble'); |
2223 | + return; |
2224 | + } |
2225 | + |
2226 | + my %callback_args = ( |
2227 | + Cxn => $self->{Cxn}, |
2228 | + tbl => $self->{tbl}, |
2229 | + NibbleIterator => $self, |
2230 | + ); |
2231 | + |
2232 | + if ($self->{nibbleno} == 0) { |
2233 | + $self->_prepare_sths(); |
2234 | + $self->_get_bounds(); |
2235 | + if ( my $callback = $self->{callbacks}->{init} ) { |
2236 | + $self->{oktonibble} = $callback->(%callback_args); |
2237 | + PTDEBUG && _d('init callback returned', $self->{oktonibble}); |
2238 | + if ( !$self->{oktonibble} ) { |
2239 | + $self->{no_more_boundaries} = 1; |
2240 | + return; |
2241 | + } |
2242 | + } |
2243 | + } |
2244 | + |
2245 | + NIBBLE: |
2246 | + while ( $self->{have_rows} || $self->_next_boundaries() ) { |
2247 | + if ( !$self->{have_rows} ) { |
2248 | + $self->{nibbleno}++; |
2249 | + PTDEBUG && _d($self->{nibble_sth}->{Statement}, 'params:', |
2250 | + join(', ', (@{$self->{lower}}, @{$self->{upper}}))); |
2251 | + if ( my $callback = $self->{callbacks}->{exec_nibble} ) { |
2252 | + $self->{have_rows} = $callback->(%callback_args); |
2253 | + } |
2254 | + else { |
2255 | + $self->{nibble_sth}->execute(@{$self->{lower}}, @{$self->{upper}}); |
2256 | + $self->{have_rows} = $self->{nibble_sth}->rows(); |
2257 | + } |
2258 | + PTDEBUG && _d($self->{have_rows}, 'rows in nibble', $self->{nibbleno}); |
2259 | + } |
2260 | + |
2261 | + if ( $self->{have_rows} ) { |
2262 | + my $row = $self->{nibble_sth}->fetchrow_arrayref(); |
2263 | + if ( $row ) { |
2264 | + $self->{rowno}++; |
2265 | + PTDEBUG && _d('Row', $self->{rowno}, 'in nibble',$self->{nibbleno}); |
2266 | + return [ @$row ]; |
2267 | + } |
2268 | + } |
2269 | + |
2270 | + PTDEBUG && _d('No rows in nibble or nibble skipped'); |
2271 | + if ( my $callback = $self->{callbacks}->{after_nibble} ) { |
2272 | + $callback->(%callback_args); |
2273 | + } |
2274 | + $self->{rowno} = 0; |
2275 | + $self->{have_rows} = 0; |
2276 | + } |
2277 | + |
2278 | + PTDEBUG && _d('Done nibbling'); |
2279 | + if ( my $callback = $self->{callbacks}->{done} ) { |
2280 | + $callback->(%callback_args); |
2281 | + } |
2282 | + |
2283 | + return; |
2284 | +} |
2285 | + |
2286 | +sub nibble_number { |
2287 | + my ($self) = @_; |
2288 | + return $self->{nibbleno}; |
2289 | +} |
2290 | + |
2291 | +sub set_nibble_number { |
2292 | + my ($self, $n) = @_; |
2293 | + die "I need a number" unless $n; |
2294 | + $self->{nibbleno} = $n; |
2295 | + PTDEBUG && _d('Set new nibble number:', $n); |
2296 | + return; |
2297 | +} |
2298 | + |
2299 | +sub nibble_index { |
2300 | + my ($self) = @_; |
2301 | + return $self->{index}; |
2302 | +} |
2303 | + |
2304 | +sub statements { |
2305 | + my ($self) = @_; |
2306 | + return { |
2307 | + nibble => $self->{nibble_sth}, |
2308 | + explain_nibble => $self->{explain_nibble_sth}, |
2309 | + upper_boundary => $self->{ub_sth}, |
2310 | + explain_upper_boundary => $self->{explain_ub_sth}, |
2311 | + } |
2312 | +} |
2313 | + |
2314 | +sub boundaries { |
2315 | + my ($self) = @_; |
2316 | + return { |
2317 | + first_lower => $self->{first_lower}, |
2318 | + lower => $self->{lower}, |
2319 | + upper => $self->{upper}, |
2320 | + next_lower => $self->{next_lower}, |
2321 | + last_upper => $self->{last_upper}, |
2322 | + }; |
2323 | +} |
2324 | + |
2325 | +sub set_boundary { |
2326 | + my ($self, $boundary, $values) = @_; |
2327 | + die "I need a boundary parameter" |
2328 | + unless $boundary; |
2329 | + die "Invalid boundary: $boundary" |
2330 | + unless $boundary =~ m/^(?:lower|upper|next_lower|last_upper)$/; |
2331 | + die "I need a values arrayref parameter" |
2332 | + unless $values && ref $values eq 'ARRAY'; |
2333 | + $self->{$boundary} = $values; |
2334 | + PTDEBUG && _d('Set new', $boundary, 'boundary:', Dumper($values)); |
2335 | + return; |
2336 | +} |
2337 | + |
2338 | +sub one_nibble { |
2339 | + my ($self) = @_; |
2340 | + return $self->{one_nibble}; |
2341 | +} |
2342 | + |
2343 | +sub chunk_size { |
2344 | + my ($self) = @_; |
2345 | + return $self->{limit} + 1; |
2346 | +} |
2347 | + |
2348 | +sub set_chunk_size { |
2349 | + my ($self, $limit) = @_; |
2350 | + return if $self->{one_nibble}; |
2351 | + die "Chunk size must be > 0" unless $limit; |
2352 | + $self->{limit} = $limit - 1; |
2353 | + PTDEBUG && _d('Set new chunk size (LIMIT):', $limit); |
2354 | + return; |
2355 | +} |
2356 | + |
2357 | +sub sql { |
2358 | + my ($self) = @_; |
2359 | + return $self->{sql}; |
2360 | +} |
2361 | + |
2362 | +sub more_boundaries { |
2363 | + my ($self) = @_; |
2364 | + return !$self->{no_more_boundaries}; |
2365 | +} |
2366 | + |
2367 | +sub row_estimate { |
2368 | + my ($self) = @_; |
2369 | + return $self->{row_est}; |
2370 | +} |
2371 | + |
2372 | +sub can_nibble { |
2373 | + my (%args) = @_; |
2374 | + my @required_args = qw(Cxn tbl chunk_size OptionParser TableParser); |
2375 | + foreach my $arg ( @required_args ) { |
2376 | + die "I need a $arg argument" unless $args{$arg}; |
2377 | + } |
2378 | + my ($cxn, $tbl, $chunk_size, $o) = @args{@required_args}; |
2379 | + |
2380 | + my ($row_est, $mysql_index) = get_row_estimate( |
2381 | + Cxn => $cxn, |
2382 | + tbl => $tbl, |
2383 | + where => $o->has('where') ? $o->get('where') : '', |
2384 | + ); |
2385 | + |
2386 | + my $one_nibble = !defined $args{one_nibble} || $args{one_nibble} |
2387 | + ? $row_est <= $chunk_size * $o->get('chunk-size-limit') |
2388 | + : 0; |
2389 | + PTDEBUG && _d('One nibble:', $one_nibble ? 'yes' : 'no'); |
2390 | + |
2391 | + if ( $args{resume} |
2392 | + && !defined $args{resume}->{lower_boundary} |
2393 | + && !defined $args{resume}->{upper_boundary} ) { |
2394 | + PTDEBUG && _d('Resuming from one nibble table'); |
2395 | + $one_nibble = 1; |
2396 | + } |
2397 | + |
2398 | + my $index = _find_best_index(%args, mysql_index => $mysql_index); |
2399 | + if ( !$index && !$one_nibble ) { |
2400 | + die "There is no good index and the table is oversized."; |
2401 | + } |
2402 | + |
2403 | + return { |
2404 | + row_est => $row_est, # nibble about this many rows |
2405 | + index => $index, # using this index |
2406 | + one_nibble => $one_nibble, # if the table fits in one nibble/chunk |
2407 | + }; |
2408 | +} |
2409 | + |
2410 | +sub _find_best_index { |
2411 | + my (%args) = @_; |
2412 | + my @required_args = qw(Cxn tbl TableParser); |
2413 | + my ($cxn, $tbl, $tp) = @args{@required_args}; |
2414 | + my $tbl_struct = $tbl->{tbl_struct}; |
2415 | + my $indexes = $tbl_struct->{keys}; |
2416 | + |
2417 | + my $want_index = $args{chunk_index}; |
2418 | + if ( $want_index ) { |
2419 | + PTDEBUG && _d('User wants to use index', $want_index); |
2420 | + if ( !exists $indexes->{$want_index} ) { |
2421 | + PTDEBUG && _d('Cannot use user index because it does not exist'); |
2422 | + $want_index = undef; |
2423 | + } |
2424 | + } |
2425 | + |
2426 | + if ( !$want_index && $args{mysql_index} ) { |
2427 | + PTDEBUG && _d('MySQL wants to use index', $args{mysql_index}); |
2428 | + $want_index = $args{mysql_index}; |
2429 | + } |
2430 | + |
2431 | + my $best_index; |
2432 | + my @possible_indexes; |
2433 | + if ( $want_index ) { |
2434 | + if ( $indexes->{$want_index}->{is_unique} ) { |
2435 | + PTDEBUG && _d('Will use wanted index'); |
2436 | + $best_index = $want_index; |
2437 | + } |
2438 | + else { |
2439 | + PTDEBUG && _d('Wanted index is a possible index'); |
2440 | + push @possible_indexes, $want_index; |
2441 | + } |
2442 | + } |
2443 | + else { |
2444 | + PTDEBUG && _d('Auto-selecting best index'); |
2445 | + foreach my $index ( $tp->sort_indexes($tbl_struct) ) { |
2446 | + if ( $index eq 'PRIMARY' || $indexes->{$index}->{is_unique} ) { |
2447 | + $best_index = $index; |
2448 | + last; |
2449 | + } |
2450 | + else { |
2451 | + push @possible_indexes, $index; |
2452 | + } |
2453 | + } |
2454 | + } |
2455 | + |
2456 | + if ( !$best_index && @possible_indexes ) { |
2457 | + PTDEBUG && _d('No PRIMARY or unique indexes;', |
2458 | + 'will use index with highest cardinality'); |
2459 | + foreach my $index ( @possible_indexes ) { |
2460 | + $indexes->{$index}->{cardinality} = _get_index_cardinality( |
2461 | + %args, |
2462 | + index => $index, |
2463 | + ); |
2464 | + } |
2465 | + @possible_indexes = sort { |
2466 | + my $cmp |
2467 | + = $indexes->{$b}->{cardinality} <=> $indexes->{$b}->{cardinality}; |
2468 | + if ( $cmp == 0 ) { |
2469 | + $cmp = scalar @{$indexes->{$b}->{cols}} |
2470 | + <=> scalar @{$indexes->{$a}->{cols}}; |
2471 | + } |
2472 | + $cmp; |
2473 | + } @possible_indexes; |
2474 | + $best_index = $possible_indexes[0]; |
2475 | + } |
2476 | + |
2477 | + PTDEBUG && _d('Best index:', $best_index); |
2478 | + return $best_index; |
2479 | +} |
2480 | + |
2481 | +sub _get_index_cardinality { |
2482 | + my (%args) = @_; |
2483 | + my @required_args = qw(Cxn tbl index); |
2484 | + my ($cxn, $tbl, $index) = @args{@required_args}; |
2485 | + |
2486 | + my $sql = "SHOW INDEXES FROM $tbl->{name} " |
2487 | + . "WHERE Key_name = '$index'"; |
2488 | + PTDEBUG && _d($sql); |
2489 | + my $cardinality = 1; |
2490 | + my $rows = $cxn->dbh()->selectall_hashref($sql, 'key_name'); |
2491 | + foreach my $row ( values %$rows ) { |
2492 | + $cardinality *= $row->{cardinality} if $row->{cardinality}; |
2493 | + } |
2494 | + PTDEBUG && _d('Index', $index, 'cardinality:', $cardinality); |
2495 | + return $cardinality; |
2496 | +} |
2497 | + |
2498 | +sub get_row_estimate { |
2499 | + my (%args) = @_; |
2500 | + my @required_args = qw(Cxn tbl); |
2501 | + foreach my $arg ( @required_args ) { |
2502 | + die "I need a $arg argument" unless $args{$arg}; |
2503 | + } |
2504 | + my ($cxn, $tbl) = @args{@required_args}; |
2505 | + |
2506 | + if ( !$args{where} && exists $tbl->{tbl_status} ) { |
2507 | + PTDEBUG && _d('Using table status for row estimate'); |
2508 | + return $tbl->{tbl_status}->{rows} || 0; |
2509 | + } |
2510 | + else { |
2511 | + PTDEBUG && _d('Use EXPLAIN for row estimate'); |
2512 | + my $sql = "EXPLAIN SELECT * FROM $tbl->{name} " |
2513 | + . "WHERE " . ($args{where} || '1=1'); |
2514 | + PTDEBUG && _d($sql); |
2515 | + my $expl = $cxn->dbh()->selectrow_hashref($sql); |
2516 | + PTDEBUG && _d(Dumper($expl)); |
2517 | + return ($expl->{rows} || 0), $expl->{key}; |
2518 | + } |
2519 | +} |
2520 | + |
2521 | +sub _prepare_sths { |
2522 | + my ($self) = @_; |
2523 | + PTDEBUG && _d('Preparing statement handles'); |
2524 | + |
2525 | + my $dbh = $self->{Cxn}->dbh(); |
2526 | + |
2527 | + $self->{nibble_sth} = $dbh->prepare($self->{nibble_sql}); |
2528 | + $self->{explain_nibble_sth} = $dbh->prepare($self->{explain_nibble_sql}); |
2529 | + |
2530 | + if ( !$self->{one_nibble} ) { |
2531 | + $self->{ub_sth} = $dbh->prepare($self->{ub_sql}); |
2532 | + $self->{explain_ub_sth} = $dbh->prepare($self->{explain_ub_sql}); |
2533 | + } |
2534 | + |
2535 | + return; |
2536 | +} |
2537 | + |
2538 | +sub _get_bounds { |
2539 | + my ($self) = @_; |
2540 | + |
2541 | + if ( $self->{one_nibble} ) { |
2542 | + if ( $self->{resume} ) { |
2543 | + $self->{no_more_boundaries} = 1; |
2544 | + } |
2545 | + return; |
2546 | + } |
2547 | + |
2548 | + my $dbh = $self->{Cxn}->dbh(); |
2549 | + |
2550 | + $self->{first_lower} = $dbh->selectrow_arrayref($self->{first_lb_sql}); |
2551 | + PTDEBUG && _d('First lower boundary:', Dumper($self->{first_lower})); |
2552 | + |
2553 | + if ( my $nibble = $self->{resume} ) { |
2554 | + if ( defined $nibble->{lower_boundary} |
2555 | + && defined $nibble->{upper_boundary} ) { |
2556 | + my $sth = $dbh->prepare($self->{resume_lb_sql}); |
2557 | + my @ub = split ',', $nibble->{upper_boundary}; |
2558 | + PTDEBUG && _d($sth->{Statement}, 'params:', @ub); |
2559 | + $sth->execute(@ub); |
2560 | + $self->{next_lower} = $sth->fetchrow_arrayref(); |
2561 | + $sth->finish(); |
2562 | + } |
2563 | + } |
2564 | + else { |
2565 | + $self->{next_lower} = $self->{first_lower}; |
2566 | + } |
2567 | + PTDEBUG && _d('Next lower boundary:', Dumper($self->{next_lower})); |
2568 | + |
2569 | + if ( !$self->{next_lower} ) { |
2570 | + PTDEBUG && _d('At end of table, or no more boundaries to resume'); |
2571 | + $self->{no_more_boundaries} = 1; |
2572 | + } |
2573 | + |
2574 | + return; |
2575 | +} |
2576 | + |
2577 | +sub _next_boundaries { |
2578 | + my ($self) = @_; |
2579 | + |
2580 | + if ( $self->{no_more_boundaries} ) { |
2581 | + PTDEBUG && _d('No more boundaries'); |
2582 | + return; # stop nibbling |
2583 | + } |
2584 | + |
2585 | + if ( $self->{one_nibble} ) { |
2586 | + $self->{lower} = $self->{upper} = []; |
2587 | + $self->{no_more_boundaries} = 1; # for next call |
2588 | + return 1; # continue nibbling |
2589 | + } |
2590 | + |
2591 | + if ( $self->identical_boundaries($self->{lower}, $self->{next_lower}) ) { |
2592 | + PTDEBUG && _d('Infinite loop detected'); |
2593 | + my $tbl = $self->{tbl}; |
2594 | + my $index = $tbl->{tbl_struct}->{keys}->{$self->{index}}; |
2595 | + my $n_cols = scalar @{$index->{cols}}; |
2596 | + my $chunkno = $self->{nibbleno}; |
2597 | + die "Possible infinite loop detected! " |
2598 | + . "The lower boundary for chunk $chunkno is " |
2599 | + . "<" . join(', ', @{$self->{lower}}) . "> and the lower " |
2600 | + . "boundary for chunk " . ($chunkno + 1) . " is also " |
2601 | + . "<" . join(', ', @{$self->{next_lower}}) . ">. " |
2602 | + . "This usually happens when using a non-unique single " |
2603 | + . "column index. The current chunk index for table " |
2604 | + . "$tbl->{db}.$tbl->{tbl} is $self->{index} which is" |
2605 | + . ($index->{is_unique} ? '' : ' not') . " unique and covers " |
2606 | + . ($n_cols > 1 ? "$n_cols columns" : "1 column") . ".\n"; |
2607 | + } |
2608 | + $self->{lower} = $self->{next_lower}; |
2609 | + |
2610 | + if ( my $callback = $self->{callbacks}->{next_boundaries} ) { |
2611 | + my $oktonibble = $callback->( |
2612 | + Cxn => $self->{Cxn}, |
2613 | + tbl => $self->{tbl}, |
2614 | + NibbleIterator => $self, |
2615 | + ); |
2616 | + PTDEBUG && _d('next_boundaries callback returned', $oktonibble); |
2617 | + if ( !$oktonibble ) { |
2618 | + $self->{no_more_boundaries} = 1; |
2619 | + return; # stop nibbling |
2620 | + } |
2621 | + } |
2622 | + |
2623 | + PTDEBUG && _d($self->{ub_sth}->{Statement}, 'params:', |
2624 | + join(', ', @{$self->{lower}}), $self->{limit}); |
2625 | + $self->{ub_sth}->execute(@{$self->{lower}}, $self->{limit}); |
2626 | + my $boundary = $self->{ub_sth}->fetchall_arrayref(); |
2627 | + PTDEBUG && _d('Next boundary:', Dumper($boundary)); |
2628 | + if ( $boundary && @$boundary ) { |
2629 | + $self->{upper} = $boundary->[0]; |
2630 | + |
2631 | + if ( $boundary->[1] ) { |
2632 | + $self->{next_lower} = $boundary->[1]; |
2633 | + } |
2634 | + else { |
2635 | + PTDEBUG && _d('End of table boundary:', Dumper($boundary->[0])); |
2636 | + $self->{no_more_boundaries} = 1; # for next call |
2637 | + |
2638 | + $self->{last_upper} = $boundary->[0]; |
2639 | + } |
2640 | + } |
2641 | + else { |
2642 | + my $dbh = $self->{Cxn}->dbh(); |
2643 | + $self->{upper} = $dbh->selectrow_arrayref($self->{last_ub_sql}); |
2644 | + PTDEBUG && _d('Last upper boundary:', Dumper($self->{upper})); |
2645 | + $self->{no_more_boundaries} = 1; # for next call |
2646 | + |
2647 | + $self->{last_upper} = $self->{upper}; |
2648 | + } |
2649 | + $self->{ub_sth}->finish(); |
2650 | + |
2651 | + return 1; # continue nibbling |
2652 | +} |
2653 | + |
2654 | +sub identical_boundaries { |
2655 | + my ($self, $b1, $b2) = @_; |
2656 | + |
2657 | + return 0 if ($b1 && !$b2) || (!$b1 && $b2); |
2658 | + |
2659 | + return 1 if !$b1 && !$b2; |
2660 | + |
2661 | + die "Boundaries have different numbers of values" |
2662 | + if scalar @$b1 != scalar @$b2; # shouldn't happen |
2663 | + my $n_vals = scalar @$b1; |
2664 | + for my $i ( 0..($n_vals-1) ) { |
2665 | + return 0 if $b1->[$i] ne $b2->[$i]; # diff |
2666 | + } |
2667 | + return 1; |
2668 | +} |
2669 | + |
2670 | +sub DESTROY { |
2671 | + my ( $self ) = @_; |
2672 | + foreach my $key ( keys %$self ) { |
2673 | + if ( $key =~ m/_sth$/ ) { |
2674 | + PTDEBUG && _d('Finish', $key); |
2675 | + $self->{$key}->finish(); |
2676 | + } |
2677 | + } |
2678 | + return; |
2679 | +} |
2680 | + |
2681 | +sub _d { |
2682 | + my ($package, undef, $line) = caller 0; |
2683 | + @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; } |
2684 | + map { defined $_ ? $_ : 'undef' } |
2685 | + @_; |
2686 | + print STDERR "# $package:$line $PID ", join(' ', @_), "\n"; |
2687 | +} |
2688 | + |
2689 | +1; |
2690 | +} |
2691 | +# ########################################################################### |
2692 | +# End NibbleIterator package |
2693 | +# ########################################################################### |
2694 | + |
2695 | +# ########################################################################### |
2696 | +# Transformers package |
2697 | +# This package is a copy without comments from the original. The original |
2698 | +# with comments and its test file can be found in the Bazaar repository at, |
2699 | +# lib/Transformers.pm |
2700 | +# t/lib/Transformers.t |
2701 | +# See https://launchpad.net/percona-toolkit for more information. |
2702 | +# ########################################################################### |
2703 | +{ |
2704 | +package Transformers; |
2705 | + |
2706 | +use strict; |
2707 | +use warnings FATAL => 'all'; |
2708 | +use English qw(-no_match_vars); |
2709 | +use constant PTDEBUG => $ENV{PTDEBUG} || 0; |
2710 | + |
2711 | +use Time::Local qw(timegm timelocal); |
2712 | +use Digest::MD5 qw(md5_hex); |
2713 | + |
2714 | +require Exporter; |
2715 | +our @ISA = qw(Exporter); |
2716 | +our %EXPORT_TAGS = (); |
2717 | +our @EXPORT = (); |
2718 | +our @EXPORT_OK = qw( |
2719 | + micro_t |
2720 | + percentage_of |
2721 | + secs_to_time |
2722 | + time_to_secs |
2723 | + shorten |
2724 | + ts |
2725 | + parse_timestamp |
2726 | + unix_timestamp |
2727 | + any_unix_timestamp |
2728 | + make_checksum |
2729 | + crc32 |
2730 | +); |
2731 | + |
2732 | +our $mysql_ts = qr/(\d\d)(\d\d)(\d\d) +(\d+):(\d+):(\d+)(\.\d+)?/; |
2733 | +our $proper_ts = qr/(\d\d\d\d)-(\d\d)-(\d\d)[T ](\d\d):(\d\d):(\d\d)(\.\d+)?/; |
2734 | +our $n_ts = qr/(\d{1,5})([shmd]?)/; # Limit \d{1,5} because \d{6} looks |
2735 | + |
2736 | +sub micro_t { |
2737 | + my ( $t, %args ) = @_; |
2738 | + my $p_ms = defined $args{p_ms} ? $args{p_ms} : 0; # precision for ms vals |
2739 | + my $p_s = defined $args{p_s} ? $args{p_s} : 0; # precision for s vals |
2740 | + my $f; |
2741 | + |
2742 | + $t = 0 if $t < 0; |
2743 | + |
2744 | + $t = sprintf('%.17f', $t) if $t =~ /e/; |
2745 | + |
2746 | + $t =~ s/\.(\d{1,6})\d*/\.$1/; |
2747 | + |
2748 | + if ($t > 0 && $t <= 0.000999) { |
2749 | + $f = ($t * 1000000) . 'us'; |
2750 | + } |
2751 | + elsif ($t >= 0.001000 && $t <= 0.999999) { |
2752 | + $f = sprintf("%.${p_ms}f", $t * 1000); |
2753 | + $f = ($f * 1) . 'ms'; # * 1 to remove insignificant zeros |
2754 | + } |
2755 | + elsif ($t >= 1) { |
2756 | + $f = sprintf("%.${p_s}f", $t); |
2757 | + $f = ($f * 1) . 's'; # * 1 to remove insignificant zeros |
2758 | + } |
2759 | + else { |
2760 | + $f = 0; # $t should = 0 at this point |
2761 | + } |
2762 | + |
2763 | + return $f; |
2764 | +} |
2765 | + |
2766 | +sub percentage_of { |
2767 | + my ( $is, $of, %args ) = @_; |
2768 | + my $p = $args{p} || 0; # float precision |
2769 | + my $fmt = $p ? "%.${p}f" : "%d"; |
2770 | + return sprintf $fmt, ($is * 100) / ($of ||= 1); |
2771 | +} |
2772 | + |
2773 | +sub secs_to_time { |
2774 | + my ( $secs, $fmt ) = @_; |
2775 | + $secs ||= 0; |
2776 | + return '00:00' unless $secs; |
2777 | + |
2778 | + $fmt ||= $secs >= 86_400 ? 'd' |
2779 | + : $secs >= 3_600 ? 'h' |
2780 | + : 'm'; |
2781 | + |
2782 | + return |
2783 | + $fmt eq 'd' ? sprintf( |
2784 | + "%d+%02d:%02d:%02d", |
2785 | + int($secs / 86_400), |
2786 | + int(($secs % 86_400) / 3_600), |
2787 | + int(($secs % 3_600) / 60), |
2788 | + $secs % 60) |
2789 | + : $fmt eq 'h' ? sprintf( |
2790 | + "%02d:%02d:%02d", |
2791 | + int(($secs % 86_400) / 3_600), |
2792 | + int(($secs % 3_600) / 60), |
2793 | + $secs % 60) |
2794 | + : sprintf( |
2795 | + "%02d:%02d", |
2796 | + int(($secs % 3_600) / 60), |
2797 | + $secs % 60); |
2798 | +} |
2799 | + |
2800 | +sub time_to_secs { |
2801 | + my ( $val, $default_suffix ) = @_; |
2802 | + die "I need a val argument" unless defined $val; |
2803 | + my $t = 0; |
2804 | + my ( $prefix, $num, $suffix ) = $val =~ m/([+-]?)(\d+)([a-z])?$/; |
2805 | + $suffix = $suffix || $default_suffix || 's'; |
2806 | + if ( $suffix =~ m/[smhd]/ ) { |
2807 | + $t = $suffix eq 's' ? $num * 1 # Seconds |
2808 | + : $suffix eq 'm' ? $num * 60 # Minutes |
2809 | + : $suffix eq 'h' ? $num * 3600 # Hours |
2810 | + : $num * 86400; # Days |
2811 | + |
2812 | + $t *= -1 if $prefix && $prefix eq '-'; |
2813 | + } |
2814 | + else { |
2815 | + die "Invalid suffix for $val: $suffix"; |
2816 | + } |
2817 | + return $t; |
2818 | +} |
2819 | + |
2820 | +sub shorten { |
2821 | + my ( $num, %args ) = @_; |
2822 | + my $p = defined $args{p} ? $args{p} : 2; # float precision |
2823 | + my $d = defined $args{d} ? $args{d} : 1_024; # divisor |
2824 | + my $n = 0; |
2825 | + my @units = ('', qw(k M G T P E Z Y)); |
2826 | + while ( $num >= $d && $n < @units - 1 ) { |
2827 | + $num /= $d; |
2828 | + ++$n; |
2829 | + } |
2830 | + return sprintf( |
2831 | + $num =~ m/\./ || $n |
2832 | + ? "%.${p}f%s" |
2833 | + : '%d', |
2834 | + $num, $units[$n]); |
2835 | +} |
2836 | + |
2837 | +sub ts { |
2838 | + my ( $time, $gmt ) = @_; |
2839 | + my ( $sec, $min, $hour, $mday, $mon, $year ) |
2840 | + = $gmt ? gmtime($time) : localtime($time); |
2841 | + $mon += 1; |
2842 | + $year += 1900; |
2843 | + my $val = sprintf("%d-%02d-%02dT%02d:%02d:%02d", |
2844 | + $year, $mon, $mday, $hour, $min, $sec); |
2845 | + if ( my ($us) = $time =~ m/(\.\d+)$/ ) { |
2846 | + $us = sprintf("%.6f", $us); |
2847 | + $us =~ s/^0\././; |
2848 | + $val .= $us; |
2849 | + } |
2850 | + return $val; |
2851 | +} |
2852 | + |
2853 | +sub parse_timestamp { |
2854 | + my ( $val ) = @_; |
2855 | + if ( my($y, $m, $d, $h, $i, $s, $f) |
2856 | + = $val =~ m/^$mysql_ts$/ ) |
2857 | + { |
2858 | + return sprintf "%d-%02d-%02d %02d:%02d:" |
2859 | + . (defined $f ? '%09.6f' : '%02d'), |
2860 | + $y + 2000, $m, $d, $h, $i, (defined $f ? $s + $f : $s); |
2861 | + } |
2862 | + return $val; |
2863 | +} |
2864 | + |
2865 | +sub unix_timestamp { |
2866 | + my ( $val, $gmt ) = @_; |
2867 | + if ( my($y, $m, $d, $h, $i, $s, $us) = $val =~ m/^$proper_ts$/ ) { |
2868 | + $val = $gmt |
2869 | + ? timegm($s, $i, $h, $d, $m - 1, $y) |
2870 | + : timelocal($s, $i, $h, $d, $m - 1, $y); |
2871 | + if ( defined $us ) { |
2872 | + $us = sprintf('%.6f', $us); |
2873 | + $us =~ s/^0\././; |
2874 | + $val .= $us; |
2875 | + } |
2876 | + } |
2877 | + return $val; |
2878 | +} |
2879 | + |
2880 | +sub any_unix_timestamp { |
2881 | + my ( $val, $callback ) = @_; |
2882 | + |
2883 | + if ( my ($n, $suffix) = $val =~ m/^$n_ts$/ ) { |
2884 | + $n = $suffix eq 's' ? $n # Seconds |
2885 | + : $suffix eq 'm' ? $n * 60 # Minutes |
2886 | + : $suffix eq 'h' ? $n * 3600 # Hours |
2887 | + : $suffix eq 'd' ? $n * 86400 # Days |
2888 | + : $n; # default: Seconds |
2889 | + PTDEBUG && _d('ts is now - N[shmd]:', $n); |
2890 | + return time - $n; |
2891 | + } |
2892 | + elsif ( $val =~ m/^\d{9,}/ ) { |
2893 | + PTDEBUG && _d('ts is already a unix timestamp'); |
2894 | + return $val; |
2895 | + } |
2896 | + elsif ( my ($ymd, $hms) = $val =~ m/^(\d{6})(?:\s+(\d+:\d+:\d+))?/ ) { |
2897 | + PTDEBUG && _d('ts is MySQL slow log timestamp'); |
2898 | + $val .= ' 00:00:00' unless $hms; |
2899 | + return unix_timestamp(parse_timestamp($val)); |
2900 | + } |
2901 | + elsif ( ($ymd, $hms) = $val =~ m/^(\d{4}-\d\d-\d\d)(?:[T ](\d+:\d+:\d+))?/) { |
2902 | + PTDEBUG && _d('ts is properly formatted timestamp'); |
2903 | + $val .= ' 00:00:00' unless $hms; |
2904 | + return unix_timestamp($val); |
2905 | + } |
2906 | + else { |
2907 | + PTDEBUG && _d('ts is MySQL expression'); |
2908 | + return $callback->($val) if $callback && ref $callback eq 'CODE'; |
2909 | + } |
2910 | + |
2911 | + PTDEBUG && _d('Unknown ts type:', $val); |
2912 | + return; |
2913 | +} |
2914 | + |
2915 | +sub make_checksum { |
2916 | + my ( $val ) = @_; |
2917 | + my $checksum = uc substr(md5_hex($val), -16); |
2918 | + PTDEBUG && _d($checksum, 'checksum for', $val); |
2919 | + return $checksum; |
2920 | +} |
2921 | + |
2922 | +sub crc32 { |
2923 | + my ( $string ) = @_; |
2924 | + return unless $string; |
2925 | + my $poly = 0xEDB88320; |
2926 | + my $crc = 0xFFFFFFFF; |
2927 | + foreach my $char ( split(//, $string) ) { |
2928 | + my $comp = ($crc ^ ord($char)) & 0xFF; |
2929 | + for ( 1 .. 8 ) { |
2930 | + $comp = $comp & 1 ? $poly ^ ($comp >> 1) : $comp >> 1; |
2931 | + } |
2932 | + $crc = (($crc >> 8) & 0x00FFFFFF) ^ $comp; |
2933 | + } |
2934 | + return $crc ^ 0xFFFFFFFF; |
2935 | +} |
2936 | + |
2937 | +sub _d { |
2938 | + my ($package, undef, $line) = caller 0; |
2939 | + @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; } |
2940 | + map { defined $_ ? $_ : 'undef' } |
2941 | + @_; |
2942 | + print STDERR "# $package:$line $PID ", join(' ', @_), "\n"; |
2943 | +} |
2944 | + |
2945 | +1; |
2946 | +} |
2947 | +# ########################################################################### |
2948 | +# End Transformers package |
2949 | +# ########################################################################### |
2950 | + |
2951 | +# ########################################################################### |
2952 | +# CleanupTask package |
2953 | +# This package is a copy without comments from the original. The original |
2954 | +# with comments and its test file can be found in the Bazaar repository at, |
2955 | +# lib/CleanupTask.pm |
2956 | +# t/lib/CleanupTask.t |
2957 | +# See https://launchpad.net/percona-toolkit for more information. |
2958 | +# ########################################################################### |
2959 | +{ |
2960 | +package CleanupTask; |
2961 | + |
2962 | +use strict; |
2963 | +use warnings FATAL => 'all'; |
2964 | +use English qw(-no_match_vars); |
2965 | +use constant PTDEBUG => $ENV{PTDEBUG} || 0; |
2966 | + |
2967 | +sub new { |
2968 | + my ( $class, $task ) = @_; |
2969 | + die "I need a task parameter" unless $task; |
2970 | + die "The task parameter must be a coderef" unless ref $task eq 'CODE'; |
2971 | + my $self = { |
2972 | + task => $task, |
2973 | + }; |
2974 | + PTDEBUG && _d('Created cleanup task', $task); |
2975 | + return bless $self, $class; |
2976 | +} |
2977 | + |
2978 | +sub DESTROY { |
2979 | + my ($self) = @_; |
2980 | + my $task = $self->{task}; |
2981 | + PTDEBUG && _d('Calling cleanup task', $task); |
2982 | + $task->(); |
2983 | + return; |
2984 | +} |
2985 | + |
2986 | +sub _d { |
2987 | + my ($package, undef, $line) = caller 0; |
2988 | + @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; } |
2989 | + map { defined $_ ? $_ : 'undef' } |
2990 | + @_; |
2991 | + print STDERR "# $package:$line $PID ", join(' ', @_), "\n"; |
2992 | +} |
2993 | + |
2994 | +1; |
2995 | +} |
2996 | +# ########################################################################### |
2997 | +# End CleanupTask package |
2998 | +# ########################################################################### |
2999 | + |
3000 | +# ########################################################################### |
3001 | # This is a combination of modules and programs in one -- a runnable module. |
3002 | # http://www.perl.com/pub/a/2006/07/13/lightning-articles.html?page=last |
3003 | # Or, look it up in the Camel book on pages 642 and 643 in the 3rd edition. |
3004 | @@ -3869,29 +5821,37 @@ |
3005 | # ########################################################################### |
3006 | package pt_online_schema_change; |
3007 | |
3008 | +use strict; |
3009 | +use warnings FATAL => 'all'; |
3010 | use English qw(-no_match_vars); |
3011 | -use Time::HiRes qw(sleep); |
3012 | +use constant PTDEBUG => $ENV{PTDEBUG} || 0; |
3013 | + |
3014 | +use Time::HiRes qw(time); |
3015 | use Data::Dumper; |
3016 | $Data::Dumper::Indent = 1; |
3017 | $Data::Dumper::Sortkeys = 1; |
3018 | $Data::Dumper::Quotekeys = 0; |
3019 | |
3020 | -Transformers->import(qw(ts)); |
3021 | - |
3022 | -use constant PTDEBUG => $ENV{PTDEBUG} || 0; |
3023 | - |
3024 | -my $quiet = 0; # for msg() |
3025 | +use sigtrap 'handler', \&sig_int, 'normal-signals'; |
3026 | + |
3027 | +my $exit_status = 0; |
3028 | +my $oktorun = 1; |
3029 | +my @drop_trigger_sqls; |
3030 | + |
3031 | +$OUTPUT_AUTOFLUSH = 1; |
3032 | |
3033 | sub main { |
3034 | - @ARGV = @_; # set global ARGV for this package |
3035 | - my $vp = new VersionParser(); |
3036 | - my $q = new Quoter(); |
3037 | - my $tp = new TableParser(Quoter => $q); |
3038 | - my $chunker = new TableChunker(Quoter => $q, TableParser => $tp); |
3039 | + # Reset global vars else tests will fail. |
3040 | + @ARGV = @_; |
3041 | + $oktorun = 1; |
3042 | + @drop_trigger_sqls = (); |
3043 | + |
3044 | + $exit_status = 0; |
3045 | |
3046 | # ######################################################################## |
3047 | # Get configuration information. |
3048 | # ######################################################################## |
3049 | + my $q = new Quoter(); |
3050 | my $o = new OptionParser(); |
3051 | $o->get_specs(); |
3052 | $o->get_opts(); |
3053 | @@ -3899,41 +5859,36 @@ |
3054 | my $dp = $o->DSNParser(); |
3055 | $dp->prop('set-vars', $o->get('set-vars')); |
3056 | |
3057 | - $quiet = $o->get('quiet'); # for msg() |
3058 | - |
3059 | - my ($dsn, $db, $tbl); |
3060 | - $dsn = shift @ARGV; |
3061 | + # The original table, i.e. the one being altered, must be specified |
3062 | + # on the command line via the DSN. |
3063 | + my ($db, $tbl); |
3064 | + my $dsn = shift @ARGV; |
3065 | if ( !$dsn ) { |
3066 | - $o->save_error('A DSN with a t part must be specified'); |
3067 | + $o->save_error('A DSN must be specified'); |
3068 | } |
3069 | else { |
3070 | + # Parse DSN string and convert it to a DSN data struct. |
3071 | $dsn = $dp->parse($dsn, $dp->parse_options($o)); |
3072 | - if ( !$dsn->{t} ) { |
3073 | - $o->save_error('The DSN must specify a t (table) part'); |
3074 | - } |
3075 | - else { |
3076 | - ($db, $tbl) = $q->split_unquote($dsn->{t} || "", $dsn->{D} || ""); |
3077 | - } |
3078 | - } |
3079 | - |
3080 | - my $rename_fk_method = lc($o->get('update-foreign-keys-method') || ''); |
3081 | - if ( ($rename_fk_method || '') eq 'drop_old_table' ) { |
3082 | - $o->set('rename-tables', 0); |
3083 | - $o->set('drop-old-table', 0), |
3084 | - } |
3085 | + $db = $dsn->{D}; |
3086 | + $tbl = $dsn->{t}; |
3087 | + } |
3088 | + |
3089 | + my $alter_fk_method = $o->get('alter-foreign-keys-method') || ''; |
3090 | + if ( $alter_fk_method eq 'drop_swap' ) { |
3091 | + $o->set('swap-tables', 0); |
3092 | + $o->set('drop-old-table', 0); |
3093 | + } |
3094 | + |
3095 | + # Explicit --chunk-size disable auto chunk sizing. |
3096 | + $o->set('chunk-time', 0) if $o->got('chunk-size'); |
3097 | |
3098 | if ( !$o->get('help') ) { |
3099 | if ( @ARGV ) { |
3100 | $o->save_error('Specify only one DSN on the command line'); |
3101 | } |
3102 | |
3103 | - if ( !$db ) { |
3104 | - $o->save_error("No database was specified in the DSN or by " |
3105 | - . "--database (-D)"); |
3106 | - } |
3107 | - |
3108 | - if ( $tbl && $tbl eq ($o->get('tmp-table') || "") ) { |
3109 | - $o->save_error("--tmp-table cannot be the same as the table"); |
3110 | + if ( !$db || !$tbl ) { |
3111 | + $o->save_error("The DSN must specify a database (D) and a table (t)"); |
3112 | } |
3113 | |
3114 | if ( $o->get('progress') ) { |
3115 | @@ -3944,523 +5899,1755 @@ |
3116 | } |
3117 | } |
3118 | |
3119 | - if ( $o->get('child-tables') && !$o->get('update-foreign-keys-method') ) { |
3120 | - $o->save_error("--child-tables requires --update-foreign-keys-method"); |
3121 | - } |
3122 | - |
3123 | - if ( $rename_fk_method |
3124 | - && $rename_fk_method ne 'rebuild_constraints' |
3125 | - && $rename_fk_method ne 'drop_old_table' ) { |
3126 | - $o->save_error("Invalid --update-foreign-keys-method value"); |
3127 | - } |
3128 | - } |
3129 | - |
3130 | - $o->usage_or_errors(); |
3131 | - |
3132 | - msg("$PROGRAM_NAME started"); |
3133 | - my $exit_status = 0; |
3134 | + # See the "pod-based-option-value-validation" spec for how this may |
3135 | + # be automagically validated. |
3136 | + if ( $alter_fk_method |
3137 | + && $alter_fk_method ne 'auto' |
3138 | + && $alter_fk_method ne 'rebuild_constraints' |
3139 | + && $alter_fk_method ne 'drop_swap' |
3140 | + && $alter_fk_method ne 'none' ) |
3141 | + { |
3142 | + $o->save_error("Invalid --alter-foreign-keys-method value: $alter_fk_method"); |
3143 | + } |
3144 | + } |
3145 | + |
3146 | + $o->usage_or_errors(); |
3147 | + |
3148 | + if ( $o->get('quiet') ) { |
3149 | + # BARON: this will fail on Windows, where there is no /dev/null. I feel |
3150 | + # it's a hack, like ignoring a problem instead of fixing it somehow. We |
3151 | + # should take a look at the things that get printed in a "normal" |
3152 | + # non-quiet run, and "if !quiet" them, and then do some kind of Logger.pm |
3153 | + # or Messager.pm module for a future release. |
3154 | + close STDOUT; |
3155 | + open STDOUT, '>', '/dev/null' |
3156 | + or warn "Cannot reopen STDOUT to /dev/null: $OS_ERROR"; |
3157 | + } |
3158 | |
3159 | # ######################################################################## |
3160 | # Connect to MySQL. |
3161 | # ######################################################################## |
3162 | - my $dbh = get_cxn( |
3163 | - dsn => $dsn, |
3164 | - DSNParser => $dp, |
3165 | + my $set_on_connect = sub { |
3166 | + my ($dbh) = @_; |
3167 | + |
3168 | + # See the same code in pt-table-checksum. |
3169 | + my $lock_wait_timeout = $o->get('lock-wait-timeout'); |
3170 | + my $set_lwt = "SET SESSION innodb_lock_wait_timeout=$lock_wait_timeout"; |
3171 | + PTDEBUG && _d($set_lwt); |
3172 | + eval { |
3173 | + $dbh->do($set_lwt); |
3174 | + }; |
3175 | + if ( $EVAL_ERROR ) { |
3176 | + PTDEBUG && _d($EVAL_ERROR); |
3177 | + # Get the server's current value. |
3178 | + my $sql = "SHOW SESSION VARIABLES LIKE 'innodb_lock_wait_timeout'"; |
3179 | + PTDEBUG && _d($dbh, $sql); |
3180 | + my (undef, $curr_lwt) = $dbh->selectrow_array($sql); |
3181 | + PTDEBUG && _d('innodb_lock_wait_timeout on server:', $curr_lwt); |
3182 | + if ( $curr_lwt > $lock_wait_timeout ) { |
3183 | + warn "Failed to $set_lwt: $EVAL_ERROR\n" |
3184 | + . "The current innodb_lock_wait_timeout value " |
3185 | + . "$curr_lwt is greater than the --lock-wait-timeout " |
3186 | + . "value $lock_wait_timeout and the variable cannot be " |
3187 | + . "changed. innodb_lock_wait_timeout is only dynamic when " |
3188 | + . "using the InnoDB plugin. To prevent this warning, either " |
3189 | + . "specify --lock-wait-time=$curr_lwt, or manually set " |
3190 | + . "innodb_lock_wait_timeout to a value less than or equal " |
3191 | + . "to $lock_wait_timeout and restart MySQL.\n"; |
3192 | + } |
3193 | + } |
3194 | + }; |
3195 | + |
3196 | + # Do not call "new Cxn(" directly; use this sub so that set_on_connect |
3197 | + # is applied to every cxn. |
3198 | + # BARON: why not make this a subroutine instead of a subroutine variable? I |
3199 | + # think that can be less confusing. Also, the $set_on_connect variable can be |
3200 | + # inlined into this subroutine. Many of our tools have a get_dbh() subroutine |
3201 | + # and it might be good to just make a convention of it. |
3202 | + my $make_cxn = sub { |
3203 | + my (%args) = @_; |
3204 | + my $cxn = new Cxn( |
3205 | + %args, |
3206 | + DSNParser => $dp, |
3207 | + OptionParser => $o, |
3208 | + set => $set_on_connect, |
3209 | + ); |
3210 | + eval { $cxn->connect() }; # connect or die trying |
3211 | + if ( $EVAL_ERROR ) { |
3212 | + die "Cannot connect to MySQL: $EVAL_ERROR\n"; |
3213 | + } |
3214 | + return $cxn; |
3215 | + }; |
3216 | + |
3217 | + my $cxn = $make_cxn->(dsn => $dsn); |
3218 | + |
3219 | + # ######################################################################## |
3220 | + # Check if MySQL is new enough to have the triggers we need. |
3221 | + # Although triggers were introduced in 5.0.2, "Prior to MySQL 5.0.10, |
3222 | + # triggers cannot contain direct references to tables by name." |
3223 | + # ######################################################################## |
3224 | + my $vp = new VersionParser(); |
3225 | + if ( !$vp->version_ge($cxn->dbh(), '5.0.10') ) { |
3226 | + die "This tool requires MySQL 5.0.10 or newer.\n"; |
3227 | + } |
3228 | + |
3229 | + # ######################################################################## |
3230 | + # Setup lag and load monitors. |
3231 | + # ######################################################################## |
3232 | + my $slaves; # all slaves that are found or specified |
3233 | + my $slave_lag_cxns; # slaves whose lag we'll check |
3234 | + my $replica_lag; # ReplicaLagWaiter object |
3235 | + my $replica_lag_pr; # Progress for ReplicaLagWaiter |
3236 | + my $sys_load; # MySQLStatusWaiter object |
3237 | + my $sys_load_pr; # Progress for MySQLStatusWaiter object |
3238 | + |
3239 | + if ( $o->get('execute') ) { |
3240 | + # ##################################################################### |
3241 | + # Find and connect to slaves. |
3242 | + # ##################################################################### |
3243 | + my $ms = new MasterSlave(); |
3244 | + $slaves = $ms->get_slaves( |
3245 | + dbh => $cxn->dbh(), |
3246 | + dsn => $cxn->dsn(), |
3247 | + OptionParser => $o, |
3248 | + DSNParser => $dp, |
3249 | + Quoter => $q, |
3250 | + make_cxn => sub { |
3251 | + return $make_cxn->(@_, prev_dsn => $cxn->dsn()); |
3252 | + }, |
3253 | + ); |
3254 | + PTDEBUG && _d(scalar @$slaves, 'slaves found'); |
3255 | + |
3256 | + if ( $o->get('check-slave-lag') ) { |
3257 | + PTDEBUG && _d('Will use --check-slave-lag to check for slave lag'); |
3258 | + my $cxn = $make_cxn->( |
3259 | + dsn_string => $o->get('check-slave-lag'), |
3260 | + prev_dsn => $cxn->dsn(), |
3261 | + ); |
3262 | + $slave_lag_cxns = [ $cxn ]; |
3263 | + } |
3264 | + else { |
3265 | + PTDEBUG && _d('Will check slave lag on all slaves'); |
3266 | + $slave_lag_cxns = $slaves; |
3267 | + } |
3268 | + |
3269 | + # ##################################################################### |
3270 | + # Check for replication filters. |
3271 | + # ##################################################################### |
3272 | + if ( $o->get('check-replication-filters') ) { |
3273 | + PTDEBUG && _d("Checking slave replication filters"); |
3274 | + my @all_repl_filters; |
3275 | + foreach my $slave ( @$slaves ) { |
3276 | + my $repl_filters = $ms->get_replication_filters( |
3277 | + dbh => $slave->dbh(), |
3278 | + ); |
3279 | + if ( keys %$repl_filters ) { |
3280 | + push @all_repl_filters, |
3281 | + { name => $slave->name(), |
3282 | + filters => $repl_filters, |
3283 | + }; |
3284 | + } |
3285 | + } |
3286 | + if ( @all_repl_filters ) { |
3287 | + my $msg = "Replication filters are set on these hosts:\n"; |
3288 | + foreach my $host ( @all_repl_filters ) { |
3289 | + my $filters = $host->{filters}; |
3290 | + $msg .= " $host->{name}\n" |
3291 | + . join("\n", map { " $_ = $host->{filters}->{$_}" } |
3292 | + keys %{$host->{filters}}) |
3293 | + . "\n"; |
3294 | + } |
3295 | + $msg .= "Please read the --check-replication-filters documentation " |
3296 | + . "to learn how to solve this problem."; |
3297 | + die $msg; |
3298 | + } |
3299 | + } |
3300 | + |
3301 | + # ##################################################################### |
3302 | + # Make a ReplicaLagWaiter to help wait for slaves after each chunk. |
3303 | + # ##################################################################### |
3304 | + my $sleep = sub { |
3305 | + # Don't let the master dbh die while waiting for slaves because we |
3306 | + # may wait a very long time for slaves. |
3307 | + my $dbh = $cxn->dbh(); |
3308 | + if ( !$dbh || !$dbh->ping() ) { |
3309 | + eval { $dbh = $cxn->connect() }; # connect or die trying |
3310 | + if ( $EVAL_ERROR ) { |
3311 | + $oktorun = 0; # flag for cleanup tasks |
3312 | + chomp $EVAL_ERROR; |
3313 | + die "Lost connection to " . $cxn->name() . " while waiting for " |
3314 | + . "replica lag ($EVAL_ERROR)\n"; |
3315 | + } |
3316 | + } |
3317 | + $dbh->do("SELECT 'pt-online-schema-change keepalive'"); |
3318 | + sleep $o->get('check-interval'); |
3319 | + return; |
3320 | + }; |
3321 | + |
3322 | + my $get_lag = sub { |
3323 | + my ($cxn) = @_; |
3324 | + my $dbh = $cxn->dbh(); |
3325 | + if ( !$dbh || !$dbh->ping() ) { |
3326 | + eval { $dbh = $cxn->connect() }; # connect or die trying |
3327 | + if ( $EVAL_ERROR ) { |
3328 | + $oktorun = 0; # flag for cleanup tasks |
3329 | + chomp $EVAL_ERROR; |
3330 | + die "Lost connection to replica " . $cxn->name() |
3331 | + . " while attempting to get its lag ($EVAL_ERROR)\n"; |
3332 | + } |
3333 | + } |
3334 | + return $ms->get_slave_lag($dbh); |
3335 | + }; |
3336 | + |
3337 | + $replica_lag = new ReplicaLagWaiter( |
3338 | + slaves => $slave_lag_cxns, |
3339 | + max_lag => $o->get('max-lag'), |
3340 | + oktorun => sub { return $oktorun }, |
3341 | + get_lag => $get_lag, |
3342 | + sleep => $sleep, |
3343 | + ); |
3344 | + |
3345 | + my $get_status; |
3346 | + { |
3347 | + my $sql = "SHOW GLOBAL STATUS LIKE ?"; |
3348 | + my $sth = $cxn->dbh()->prepare($sql); |
3349 | + |
3350 | + $get_status = sub { |
3351 | + my ($var) = @_; |
3352 | + PTDEBUG && _d($sth->{Statement}, $var); |
3353 | + $sth->execute($var); |
3354 | + my (undef, $val) = $sth->fetchrow_array(); |
3355 | + return $val; |
3356 | + }; |
3357 | + } |
3358 | + |
3359 | + $sys_load = new MySQLStatusWaiter( |
3360 | + max_spec => $o->get('max-load'), |
3361 | + critical_spec => $o->get('critical-load'), |
3362 | + get_status => $get_status, |
3363 | + oktorun => sub { return $oktorun }, |
3364 | + sleep => $sleep, |
3365 | + ); |
3366 | + |
3367 | + if ( $o->get('progress') ) { |
3368 | + $replica_lag_pr = new Progress( |
3369 | + jobsize => scalar @$slaves, |
3370 | + spec => $o->get('progress'), |
3371 | + name => "Waiting for replicas to catch up", # not used |
3372 | + ); |
3373 | + |
3374 | + $sys_load_pr = new Progress( |
3375 | + jobsize => scalar @{$o->get('max-load')}, |
3376 | + spec => $o->get('progress'), |
3377 | + name => "Waiting for --max-load", # not used |
3378 | + ); |
3379 | + } |
3380 | + } |
3381 | + |
3382 | + # ######################################################################## |
3383 | + # Setup and check the original table. |
3384 | + # ######################################################################## |
3385 | + my $tp = new TableParser(Quoter => $q); |
3386 | + |
3387 | + # Common table data struct (that modules like NibbleIterator expect). |
3388 | + my $orig_tbl = { |
3389 | + db => $db, |
3390 | + tbl => $tbl, |
3391 | + name => $q->quote($db, $tbl), |
3392 | + }; |
3393 | + |
3394 | + check_orig_table( |
3395 | + orig_tbl => $orig_tbl, |
3396 | + Cxn => $cxn, |
3397 | OptionParser => $o, |
3398 | - AutoCommit => 1, |
3399 | - ); |
3400 | - msg("USE `$db`"); |
3401 | - $dbh->do("USE `$db`"); |
3402 | - |
3403 | - # ######################################################################## |
3404 | - # Daemonize only after (potentially) asking for passwords for --ask-pass. |
3405 | + TableParser => $tp, |
3406 | + Quoter => $q, |
3407 | + ); |
3408 | + |
3409 | + # ######################################################################## |
3410 | + # Get child tables of the original table, if necessary. |
3411 | + # ######################################################################## |
3412 | + my $child_tables = find_child_tables( |
3413 | + tbl => $orig_tbl, |
3414 | + Cxn => $cxn, |
3415 | + Quoter => $q, |
3416 | + ); |
3417 | + if ( !$child_tables ) { |
3418 | + if ( $alter_fk_method ) { |
3419 | + warn "No foreign keys reference $orig_tbl->{name}; ignoring " |
3420 | + . "--alter-foreign-keys-method.\n"; |
3421 | + |
3422 | + if ( $alter_fk_method eq 'drop_swap' ) { |
3423 | + # These opts are disabled at the start if the user specifies |
3424 | + # the drop_swap method, but now that we know there are no |
3425 | + # child tables, we must re-enable these to make the alter work. |
3426 | + $o->set('swap-tables', 1); |
3427 | + $o->set('drop-old-table', 1); |
3428 | + } |
3429 | + |
3430 | + $alter_fk_method = ''; |
3431 | + } |
3432 | + # No child tables and --alter-fk-method wasn't specified, |
3433 | + # so nothing to do. |
3434 | + } |
3435 | + else { |
3436 | + print "Child tables:\n"; |
3437 | + foreach my $child_table ( @$child_tables ) { |
3438 | + printf " %s (approx. %s rows)\n", |
3439 | + $child_table->{name}, |
3440 | + $child_table->{row_est} || '?'; |
3441 | + } |
3442 | + |
3443 | + if ( $alter_fk_method ) { |
3444 | + # Let the user know how we're going to update the child table fk refs. |
3445 | + my $choice |
3446 | + = $alter_fk_method eq 'none' ? "not" |
3447 | + : $alter_fk_method eq 'auto' ? "automatically choose the method to" |
3448 | + : "use the $alter_fk_method method to"; |
3449 | + print "Will $choice update foreign keys.\n"; |
3450 | + } |
3451 | + else { |
3452 | + print "You did not specify --alter-foreign-keys-method, but there " |
3453 | + . "are foreign keys that reference the table. " |
3454 | + . "Please read the tool's documentation carefully.\n"; |
3455 | + return 1; |
3456 | + } |
3457 | + } |
3458 | + |
3459 | + # ######################################################################## |
3460 | + # XXX |
3461 | + # Ready to begin the alter! Nothing has been changed on the server at |
3462 | + # this point; we've just checked and looked for things. Past this point, |
3463 | + # the code is live if --execute, else it's doing a --dry-run. Or, if |
3464 | + # the user didn't read the docs, we may bail out here. |
3465 | + # XXX |
3466 | + # ######################################################################## |
3467 | + if ( $o->get('dry-run') ) { |
3468 | + print "Starting a dry run. $orig_tbl->{name} will not be altered. " |
3469 | + . "Specify --execute instead of --dry-run to alter the table.\n"; |
3470 | + } |
3471 | + elsif ( $o->get('execute') ) { |
3472 | + print "Altering $orig_tbl->{name}...\n"; |
3473 | + } |
3474 | + else { |
3475 | + print "Exiting without altering $orig_tbl->{name} because neither " |
3476 | + . "--dry-run nor --execute was specified. Please read the tool's " |
3477 | + . "documentation carefully before using this tool.\n"; |
3478 | + return 1; |
3479 | + } |
3480 | + |
3481 | + # ######################################################################## |
3482 | + # Create a cleanup task object to undo changes (i.e. clean up) if the |
3483 | + # code dies, or we may call this explicitly at the end if all goes well. |
3484 | + # ######################################################################## |
3485 | + my @cleanup_tasks; |
3486 | + my $cleanup = new CleanupTask( |
3487 | + sub { |
3488 | + # XXX We shouldn't copy $EVAL_ERROR here, but I found that |
3489 | + # errors are not re-thrown in tests. If you comment out this |
3490 | + # line and the die below, an error fails: |
3491 | + # not ok 5 - Doesn't try forever to find a new table name |
3492 | + # Failed test 'Doesn't try forever to find a new table name' |
3493 | + # at /Users/daniel/p/pt-osc-2.1.1/lib/PerconaTest.pm line 559. |
3494 | + # '' |
3495 | + # doesn't match '(?-xism:Failed to find a unique new table name)' |
3496 | + my $original_error = $EVAL_ERROR; |
3497 | + foreach my $task ( reverse @cleanup_tasks ) { |
3498 | + eval { |
3499 | + $task->(); |
3500 | + }; |
3501 | + if ( $EVAL_ERROR ) { |
3502 | + warn "Error cleaning up: $EVAL_ERROR\n"; |
3503 | + } |
3504 | + } |
3505 | + die $original_error if $original_error; # rethrow original error |
3506 | + return; |
3507 | + } |
3508 | + ); |
3509 | + |
3510 | + # The last cleanup task is to report whether or not the orig table |
3511 | + # was altered. |
3512 | + push @cleanup_tasks, sub { |
3513 | + PTDEBUG && _d('Clean up done, report if orig table was altered'); |
3514 | + if ( $o->get('dry-run') ) { |
3515 | + print "Dry run complete. $orig_tbl->{name} was not altered.\n"; |
3516 | + } |
3517 | + else { |
3518 | + if ( $orig_tbl->{swapped} ) { |
3519 | + if ( $orig_tbl->{success} ) { |
3520 | + print "Successfully altered $orig_tbl->{name}.\n"; |
3521 | + } |
3522 | + else { |
3523 | + print "Altered $orig_tbl->{name} but there were errors " |
3524 | + . "or warnings.\n"; |
3525 | + } |
3526 | + } |
3527 | + else { |
3528 | + print "$orig_tbl->{name} was not altered.\n"; |
3529 | + } |
3530 | + } |
3531 | + return; |
3532 | + }; |
3533 | + |
3534 | + # ######################################################################## |
3535 | + # Check and create PID file if user specified --pid. |
3536 | # ######################################################################## |
3537 | my $daemon; |
3538 | - if ( $o->get('pid') ) { |
3539 | + if ( $o->get('execute') && $o->get('pid') ) { |
3540 | # We're not daemoninzing, it just handles PID stuff. |
3541 | $daemon = new Daemon(o=>$o); |
3542 | $daemon->make_PID_file(); |
3543 | } |
3544 | |
3545 | - # ######################################################################## |
3546 | - # Setup/init some vars. |
3547 | - # ######################################################################## |
3548 | - my $tmp_tbl = $o->get('tmp-table') || "__tmp_$tbl"; |
3549 | - my $old_tbl = "__old_$tbl"; # what tbl becomes after swapped with tmp tbl |
3550 | - my %tables = ( |
3551 | - db => $db, |
3552 | - tbl => $tbl, |
3553 | - tmp_tbl => $tmp_tbl, |
3554 | - old_tbl => $old_tbl, |
3555 | - ); |
3556 | - msg("Alter table $tbl using temporary table $tmp_tbl"); |
3557 | - |
3558 | - my %common_modules = ( |
3559 | - OptionParser => $o, |
3560 | - DSNParser => $dp, |
3561 | - Quoter => $q, |
3562 | - TableParser => $tp, |
3563 | - TableChunker => $chunker, |
3564 | - VersionParser => $vp, |
3565 | - ); |
3566 | - |
3567 | - # ######################################################################## |
3568 | - # Create the capture-sync and copy-rows plugins. Currently, we just have |
3569 | - # one method for each. |
3570 | - # ######################################################################## |
3571 | - my $capture_sync = new OSCCaptureSync(Quoter => $q); |
3572 | - my $copy_rows = new CopyRowsInsertSelect( |
3573 | - Retry => new Retry(), |
3574 | - Quoter => $q, |
3575 | - ); |
3576 | - |
3577 | - # More values are added later. These are the minimum need to do --cleanup. |
3578 | - my %plugin_args = ( |
3579 | - dbh => $dbh, |
3580 | - msg => \&msg, # so plugin can talk back to user |
3581 | - print => $o->get('print'), |
3582 | - %tables, |
3583 | - %common_modules, |
3584 | - ); |
3585 | - |
3586 | - if ( my $sleep_time = $o->get('sleep') ) { |
3587 | - PTDEBUG && _d("Sleep time:", $sleep_time); |
3588 | - $plugin_args{sleep} = sub { |
3589 | - my ( $chunkno ) = @_; |
3590 | - PTDEBUG && _d("Sleeping after chunk", $chunkno); |
3591 | - sleep($sleep_time); |
3592 | + # ##################################################################### |
3593 | + # Step 1: Create the new table. |
3594 | + # ##################################################################### |
3595 | + my $new_tbl; |
3596 | + eval { |
3597 | + $new_tbl = create_new_table( |
3598 | + orig_tbl => $orig_tbl, |
3599 | + suffix => '_new', |
3600 | + Cxn => $cxn, |
3601 | + Quoter => $q, |
3602 | + OptionParser => $o, |
3603 | + TableParser => $tp, |
3604 | + ); |
3605 | + }; |
3606 | + if ( $EVAL_ERROR ) { |
3607 | + die "Error creating new table: $EVAL_ERROR\n"; |
3608 | + } |
3609 | + |
3610 | + # If the new table still exists, drop it unless the tool was interrupted. |
3611 | + push @cleanup_tasks, sub { |
3612 | + PTDEBUG && _('Clean up new table'); |
3613 | + my $new_tbl_exists = $tp->check_table( |
3614 | + dbh => $cxn->dbh(), |
3615 | + db => $new_tbl->{db}, |
3616 | + tbl => $new_tbl->{tbl}, |
3617 | + ); |
3618 | + PTDEBUG && _d('New table exists:', $new_tbl_exists ? 'yes' : 'no'); |
3619 | + return unless $new_tbl_exists; |
3620 | + |
3621 | + my $sql = "DROP TABLE IF EXISTS $new_tbl->{name};"; |
3622 | + if ( !$oktorun ) { |
3623 | + # The tool was interrupted, so do not drop the new table |
3624 | + # in case the user wants to resume (once resume capability |
3625 | + # is implemented). |
3626 | + print "Not dropping the new table $new_tbl->{name} because " |
3627 | + . "the tool was interrupted. To drop the new table, " |
3628 | + . "execute:\n$sql\n"; |
3629 | + } |
3630 | + elsif ( $orig_tbl->{copied} && !$orig_tbl->{swapped} ) { |
3631 | + print "Not dropping the new table $new_tbl->{name} because " |
3632 | + . "--swap-tables failed. To drop the new table, " |
3633 | + . "execute:\n$sql\n"; |
3634 | + } |
3635 | + else { |
3636 | + print "Dropping new table...\n"; |
3637 | + print $sql, "\n" if $o->get('print'); |
3638 | + PTDEBUG && _d($sql); |
3639 | + eval { |
3640 | + $cxn->dbh()->do($sql); |
3641 | + }; |
3642 | + if ( $EVAL_ERROR ) { |
3643 | + warn "Error dropping new table $new_tbl->{name}: $EVAL_ERROR\n" |
3644 | + . "To try dropping the new table again, execute:\n$sql\n"; |
3645 | + } |
3646 | + print "Dropped new table OK.\n"; |
3647 | + } |
3648 | + }; |
3649 | + |
3650 | + # ##################################################################### |
3651 | + # Step 2: Alter the new, empty table. This should be very quick, |
3652 | + # or die if the user specified a bad alter statement. |
3653 | + # ##################################################################### |
3654 | + if ( my $alter = $o->get('alter') ) { |
3655 | + print "Altering new table...\n"; |
3656 | + my $sql = "ALTER TABLE $new_tbl->{name} $alter"; |
3657 | + print $sql, "\n" if $o->get('print'); |
3658 | + PTDEBUG && _d($sql); |
3659 | + eval { |
3660 | + $cxn->dbh()->do($sql); |
3661 | }; |
3662 | - } |
3663 | - |
3664 | - # ######################################################################## |
3665 | - # Just cleanup and exit. |
3666 | - # ######################################################################## |
3667 | - if ( $o->get('cleanup-and-exit') ) { |
3668 | - msg("Calling " . (ref $copy_rows). "::cleanup()"); |
3669 | - $copy_rows->cleanup(%plugin_args); |
3670 | - |
3671 | - msg("Calling " . (ref $capture_sync) . "::cleanup()"); |
3672 | - $capture_sync->cleanup(%plugin_args); |
3673 | - |
3674 | - msg("$PROGRAM_NAME ending for --cleanup-and-exit"); |
3675 | - return 0; |
3676 | - } |
3677 | - |
3678 | - # ######################################################################## |
3679 | - # Check that table can be altered. |
3680 | - # ######################################################################## |
3681 | - my %tbl_info; |
3682 | + if ( $EVAL_ERROR ) { |
3683 | + die "Error altering new table $new_tbl->{name}: $EVAL_ERROR\n" |
3684 | + } |
3685 | + print "Altered $new_tbl->{name} OK.\n" |
3686 | + } |
3687 | + |
3688 | + # Get the new table struct. This shouldn't die because |
3689 | + # we just created the table successfully so we know it's |
3690 | + # there. But the ghost of Ryan is everywhere. |
3691 | + my $ddl = $tp->get_create_table( |
3692 | + $cxn->dbh(), |
3693 | + $new_tbl->{db}, |
3694 | + $new_tbl->{tbl}, |
3695 | + ); |
3696 | + $new_tbl->{tbl_struct} = $tp->parse($ddl); |
3697 | + |
3698 | + # Determine what columns the original and new table share. |
3699 | + # If the user drops a col, that's easy: just don't copy it. If they |
3700 | + # add a column, it must have a default value. Other alterations |
3701 | + # may or may not affect the copy process--we'll know when we try! |
3702 | + # Note: we don't want to examine the --alter statement to see if the |
3703 | + # cols have changed because that's messy and prone to parsing errors. |
3704 | + # Col posn (position) is just for looks because user's like |
3705 | + # to see columns listed in their original order, not Perl's |
3706 | + # random hash key sorting. |
3707 | + my $col_posn = $orig_tbl->{tbl_struct}->{col_posn}; |
3708 | + my $orig_cols = $orig_tbl->{tbl_struct}->{is_col}; |
3709 | + my $new_cols = $new_tbl->{tbl_struct}->{is_col}; |
3710 | + my @common_cols = sort { $col_posn->{$a} <=> $col_posn->{$b} } |
3711 | + grep { $new_cols->{$_} } |
3712 | + keys %$orig_cols; |
3713 | + PTDEBUG && _d('Common columns', @common_cols); |
3714 | + |
3715 | + # ######################################################################## |
3716 | + # Step 3: Create the triggers to capture changes on the original table and |
3717 | + # apply them to the new table. |
3718 | + # ######################################################################## |
3719 | + |
3720 | + # Drop the triggers. We can save this cleanup task before |
3721 | + # adding the triggers because if adding them fails, this will be |
3722 | + # called which will drop whichever triggers were created. |
3723 | + push @cleanup_tasks, sub { |
3724 | + PTDEBUG && _d('Clean up triggers'); |
3725 | + if ( $oktorun ) { |
3726 | + drop_triggers( |
3727 | + tbl => $orig_tbl, |
3728 | + Cxn => $cxn, |
3729 | + Quoter => $q, |
3730 | + OptionParser => $o, |
3731 | + ); |
3732 | + } |
3733 | + else { |
3734 | + print "Not dropping triggers because the tool was interrupted. " |
3735 | + . "To drop the triggers, execute:\n" |
3736 | + . join("\n", @drop_trigger_sqls) . "\n"; |
3737 | + } |
3738 | + }; |
3739 | + |
3740 | eval { |
3741 | - %tbl_info = check_tables(%plugin_args); |
3742 | + create_triggers( |
3743 | + orig_tbl => $orig_tbl, |
3744 | + new_tbl => $new_tbl, |
3745 | + columns => \@common_cols, |
3746 | + Cxn => $cxn, |
3747 | + Quoter => $q, |
3748 | + OptionParser => $o, |
3749 | + ); |
3750 | }; |
3751 | if ( $EVAL_ERROR ) { |
3752 | - chomp $EVAL_ERROR; |
3753 | - msg("Table $tbl cannot be altered: $EVAL_ERROR"); |
3754 | - return 1; |
3755 | - } |
3756 | - |
3757 | - @plugin_args{keys %tbl_info} = values %tbl_info; |
3758 | - msg("Table $tbl can be altered"); |
3759 | - msg("Chunk column $plugin_args{chunk_column}, index $plugin_args{chunk_index}"); |
3760 | - |
3761 | - if ( $o->get('check-tables-and-exit') ) { |
3762 | - msg("$PROGRAM_NAME ending for --check-tables-and-exit"); |
3763 | - return 0; |
3764 | - } |
3765 | - |
3766 | - # ##################################################################### |
3767 | - # Chunk the table. If the checks pass, then this shouldn't fail. |
3768 | - # ##################################################################### |
3769 | - my %range_stats = $chunker->get_range_statistics( |
3770 | - dbh => $dbh, |
3771 | - db => $db, |
3772 | - tbl => $tbl, |
3773 | - chunk_col => $plugin_args{chunk_column}, |
3774 | - tbl_struct => $plugin_args{tbl_struct}, |
3775 | - ); |
3776 | - my @chunks = $chunker->calculate_chunks( |
3777 | - dbh => $dbh, |
3778 | - db => $db, |
3779 | - tbl => $tbl, |
3780 | - chunk_col => $plugin_args{chunk_column}, |
3781 | - tbl_struct => $plugin_args{tbl_struct}, |
3782 | - chunk_size => $o->get('chunk-size'), |
3783 | - %range_stats, |
3784 | - ); |
3785 | - $plugin_args{chunks} = \@chunks; |
3786 | - $plugin_args{Progress} = new Progress( |
3787 | - jobsize => scalar @chunks, |
3788 | - spec => $o->get('progress'), |
3789 | - name => "Copying rows", |
3790 | - ); |
3791 | - msg("Chunked table $tbl into " . scalar @chunks . " chunks"); |
3792 | - |
3793 | - # ##################################################################### |
3794 | - # Get child tables if necessary. |
3795 | - # ##################################################################### |
3796 | - my @child_tables; |
3797 | - if ( my $child_tables = $o->get('child-tables') ) { |
3798 | - if ( lc $child_tables eq 'auto_detect' ) { |
3799 | - msg("Auto-detecting child tables of $tbl"); |
3800 | - @child_tables = get_child_tables(%plugin_args); |
3801 | - msg("Child tables of $tables{old_tbl}: " |
3802 | - . (@child_tables ? join(', ', @child_tables) : "(none)")); |
3803 | - } |
3804 | - else { |
3805 | - @child_tables = split(',', $child_tables); |
3806 | - msg("User-specified child tables: " . join(', ', @child_tables)); |
3807 | - } |
3808 | - } |
3809 | - |
3810 | - # ##################################################################### |
3811 | - # Do the online alter. |
3812 | - # ##################################################################### |
3813 | - if ( !$o->get('execute') ) { |
3814 | - msg("Exiting without altering $db.$tbl because you did not " |
3815 | - . "specify --execute. Please read the tool's documentation " |
3816 | - . "carefully before using this tool."); |
3817 | - return $exit_status; |
3818 | - } |
3819 | - |
3820 | - msg("Starting online schema change"); |
3821 | - eval { |
3822 | - my $sql = ""; |
3823 | - |
3824 | - # ##################################################################### |
3825 | - # Create and alter the new table. |
3826 | - # ##################################################################### |
3827 | - if ( $o->get('create-tmp-table') ) { |
3828 | - $sql = "CREATE TABLE `$db`.`$tmp_tbl` LIKE `$db`.`$tbl`"; |
3829 | - msg($sql); |
3830 | - $dbh->do($sql) unless $o->get('print'); |
3831 | - } |
3832 | - |
3833 | - if ( my $alter = $o->get('alter') ) { |
3834 | - my @stmts; |
3835 | - if ( -f $alter && -r $alter ) { |
3836 | - msg("Reading ALTER TABLE statements from file $alter"); |
3837 | - open my $fh, '<', $alter or die "Cannot open $alter: $OS_ERROR"; |
3838 | - @stmts = <$fh>; |
3839 | - close $fh; |
3840 | + die "Error creating triggers: $EVAL_ERROR\n"; |
3841 | + }; |
3842 | + |
3843 | + # ##################################################################### |
3844 | + # Step 4: Copy rows. |
3845 | + # ##################################################################### |
3846 | + |
3847 | + # The hashref of callbacks below is what NibbleIterator calls internally |
3848 | + # to do all the copy work. The callbacks do not need to eval their work |
3849 | + # because the higher call to $nibble_iter->next() is eval'ed which will |
3850 | + # catch any errors in the callbacks. |
3851 | + my $total_rows = 0; |
3852 | + my $total_time = 0; |
3853 | + my $avg_rate = 0; # rows/second |
3854 | + my $retry = new Retry(); # for retrying to exec the copy statement |
3855 | + my $limit = $o->get('chunk-size-limit'); # brevity |
3856 | + my $chunk_time = $o->get('chunk-time'); # brevity |
3857 | + |
3858 | + my $callbacks = { |
3859 | + init => sub { |
3860 | + my (%args) = @_; |
3861 | + my $tbl = $args{tbl}; |
3862 | + my $nibble_iter = $args{NibbleIterator}; |
3863 | + |
3864 | + if ( $o->get('dry-run') ) { |
3865 | + print "Not copying rows because this is a dry run.\n"; |
3866 | } |
3867 | else { |
3868 | - @stmts = split(';', $alter); |
3869 | - } |
3870 | - |
3871 | - foreach my $stmt ( @stmts ) { |
3872 | - $sql = "ALTER TABLE `$db`.`$tmp_tbl` $stmt"; |
3873 | - msg($sql); |
3874 | - $dbh->do($sql) unless $o->get('print'); |
3875 | + print "Copying approximately ", $nibble_iter->row_estimate(), |
3876 | + " rows...\n"; |
3877 | + } |
3878 | + |
3879 | + if ( $o->get('print') ) { |
3880 | + # Print the checksum and next boundary statements. |
3881 | + my $statements = $nibble_iter->statements(); |
3882 | + foreach my $sth ( sort keys %$statements ) { |
3883 | + next if $sth =~ m/^explain/; |
3884 | + if ( $statements->{$sth} ) { |
3885 | + print $statements->{$sth}->{Statement}, "\n"; |
3886 | + } |
3887 | + } |
3888 | + } |
3889 | + |
3890 | + return unless $o->get('execute'); |
3891 | + |
3892 | + # If table is a single chunk on the master, make sure it's also |
3893 | + # a single chunk on all slaves. E.g. if a slave is out of sync |
3894 | + # and has a lot more rows than the master, single chunking on the |
3895 | + # master could cause the slave to choke. |
3896 | + if ( $nibble_iter->one_nibble() ) { |
3897 | + PTDEBUG && _d('Getting table row estimate on replicas'); |
3898 | + my @too_large; |
3899 | + foreach my $slave ( @$slaves ) { |
3900 | + my ($n_rows) = NibbleIterator::get_row_estimate( |
3901 | + Cxn => $slave, |
3902 | + tbl => $tbl, |
3903 | + ); |
3904 | + PTDEBUG && _d('Table on',$slave->name(),'has', $n_rows, 'rows'); |
3905 | + if ( $n_rows && $n_rows > ($tbl->{chunk_size} * $limit) ) { |
3906 | + PTDEBUG && _d('Table too large on', $slave->name()); |
3907 | + push @too_large, [$slave->name(), $n_rows || 0]; |
3908 | + } |
3909 | + } |
3910 | + if ( @too_large ) { |
3911 | + my $msg |
3912 | + = "Cannot copy table $tbl->{name} because" |
3913 | + . " on the master it would be checksummed in one chunk" |
3914 | + . " but on these replicas it has too many rows:\n"; |
3915 | + foreach my $info ( @too_large ) { |
3916 | + $msg .= " $info->[1] rows on $info->[0]\n"; |
3917 | + } |
3918 | + $msg .= "The current chunk size limit is " |
3919 | + . ($tbl->{chunk_size} * $limit) |
3920 | + . " rows (chunk size=$tbl->{chunk_size}" |
3921 | + . " * chunk size limit=$limit).\n"; |
3922 | + die $msg; |
3923 | + } |
3924 | + } |
3925 | + |
3926 | + return 1; # continue nibbling table |
3927 | + }, |
3928 | + next_boundaries => sub { |
3929 | + my (%args) = @_; |
3930 | + my $tbl = $args{tbl}; |
3931 | + my $nibble_iter = $args{NibbleIterator}; |
3932 | + my $sth = $nibble_iter->statements(); |
3933 | + my $boundary = $nibble_iter->boundaries(); |
3934 | + |
3935 | + return 0 if $o->get('dry-run'); |
3936 | + return 1 if $nibble_iter->one_nibble(); |
3937 | + |
3938 | + # Check that MySQL will use the nibble index for the next upper |
3939 | + # boundary sql. This check applies to the next nibble. So if |
3940 | + # the current nibble number is 5, then nibble 5 is already done |
3941 | + # and we're checking nibble number 6. |
3942 | + my $expl = explain_statement( |
3943 | + tbl => $tbl, |
3944 | + sth => $sth->{explain_upper_boundary}, |
3945 | + vals => [ @{$boundary->{lower}}, $nibble_iter->chunk_size() ], |
3946 | + ); |
3947 | + if (lc($expl->{key} || '') ne lc($nibble_iter->nibble_index() || '')) { |
3948 | + my $msg |
3949 | + = "Aborting copying table $tbl->{name} at chunk " |
3950 | + . ($nibble_iter->nibble_number() + 1) |
3951 | + . " because it is not safe to ascend. Chunking should " |
3952 | + . "use the " |
3953 | + . ($nibble_iter->nibble_index() || '?') |
3954 | + . " index, but MySQL EXPLAIN reports that " |
3955 | + . ($expl->{key} ? "the $expl->{key}" : "no") |
3956 | + . " index will be used for " |
3957 | + . $sth->{upper_boundary}->{Statement} |
3958 | + . " with values " |
3959 | + . join(", ", map { defined $_ ? $_ : "NULL" } |
3960 | + (@{$boundary->{lower}}, $nibble_iter->chunk_size())) |
3961 | + . "\n"; |
3962 | + die $msg; |
3963 | } |
3964 | - } |
3965 | - |
3966 | - # ##################################################################### |
3967 | - # Determine what columns the two tables have in common. |
3968 | - # ##################################################################### |
3969 | - my @columns; |
3970 | - # If --print is in effect, then chances are the new table wasn't |
3971 | - # created above, so we can't get it's struct. |
3972 | - # TODO: check if the new table exists because user might have created |
3973 | - # it manually. |
3974 | - if ( !$o->get('print') ) { |
3975 | - my $tmp_tbl_struct = $tp->parse( |
3976 | - $tp->get_create_table($dbh, $db, $tmp_tbl)); |
3977 | - |
3978 | - @columns = intersection([ |
3979 | - $plugin_args{tbl_struct}->{is_col}, |
3980 | - $tmp_tbl_struct->{is_col}, |
3981 | - ]); |
3982 | - |
3983 | - # Order columns according to new table because people like/expect |
3984 | - # to see things in a certain order (this has been an issue before). |
3985 | - # This just matters to us; does't make a difference to MySQL. |
3986 | - my $col_posn = $plugin_args{tbl_struct}->{col_posn}; |
3987 | - @columns = sort { $col_posn->{$a} <=> $col_posn->{$b} } @columns; |
3988 | - msg("Shared columns: " . join(', ', @columns)); |
3989 | - } |
3990 | - $plugin_args{columns} = \@columns; |
3991 | - |
3992 | - # ##################################################################### |
3993 | - # Start capturing changes to the new table. |
3994 | - # ##################################################################### |
3995 | - msg("Calling " . (ref $capture_sync) . "::capture()"); |
3996 | - $capture_sync->capture(%plugin_args); |
3997 | - |
3998 | - # ##################################################################### |
3999 | - # Copy rows from new table to old table. |
4000 | - # ##################################################################### |
4001 | - msg("Calling " . (ref $copy_rows) . "::copy()"); |
4002 | - $copy_rows->copy( |
4003 | - from_table => $q->quote($db, $tbl), |
4004 | - to_table => $q->quote($db, $tmp_tbl), |
4005 | - %plugin_args |
4006 | - ); |
4007 | - |
4008 | - # ##################################################################### |
4009 | - # Sync tables. |
4010 | - # ##################################################################### |
4011 | - msg("Calling " . (ref $capture_sync) . "::sync()"); |
4012 | - $capture_sync->sync(%plugin_args); |
4013 | - |
4014 | - # ##################################################################### |
4015 | - # Rename tables. |
4016 | - # ##################################################################### |
4017 | - if ( $o->get('rename-tables') ) { |
4018 | - msg("Renaming tables"); |
4019 | - $sql = "RENAME TABLE `$db`.`$tbl` TO `$db`.`$old_tbl`," |
4020 | - . " `$db`.`$tmp_tbl` TO `$db`.`$tbl`"; |
4021 | - msg($sql); |
4022 | - $dbh->do($sql) unless $o->get('print'); |
4023 | - msg("Original table $tbl renamed to $old_tbl"); |
4024 | - } |
4025 | - |
4026 | - # ##################################################################### |
4027 | - # Update foreign key constraints if there are child tables. |
4028 | - # ##################################################################### |
4029 | - if ( @child_tables ) { |
4030 | - msg("Renaming foreign key constraints in child table"); |
4031 | - if ( $rename_fk_method eq 'rebuild_constraints' ) { |
4032 | - update_foreign_key_constraints( |
4033 | - child_tables => \@child_tables, |
4034 | - %plugin_args, |
4035 | - ); |
4036 | - } |
4037 | - elsif ( $rename_fk_method eq 'drop_old_table' ) { |
4038 | - $sql = "SET foreign_key_checks=0"; |
4039 | - msg($sql); |
4040 | - $dbh->do($sql) unless $o->get('print'); |
4041 | - |
4042 | - $sql = "DROP TABLE IF EXISTS `$db`.`$tbl`"; |
4043 | - msg($sql); |
4044 | - $dbh->do($sql) unless $o->get('print'); |
4045 | - |
4046 | - $sql = "RENAME TABLE `$db`.`$tmp_tbl` TO `$db`.`$tbl`"; |
4047 | - msg($sql); |
4048 | - $dbh->do($sql) unless $o->get('print'); |
4049 | + |
4050 | + # Once nibbling begins for a table, control does not return to this |
4051 | + # tool until nibbling is done because, as noted above, all work is |
4052 | + # done in these callbacks. This callback is the only place where we |
4053 | + # can prematurely stop nibbling by returning false. This allows |
4054 | + # Ctrl-C to stop the tool between nibbles instead of between tables. |
4055 | + return $oktorun; # continue nibbling table? |
4056 | + }, |
4057 | + exec_nibble => sub { |
4058 | + my (%args) = @_; |
4059 | + my $tbl = $args{tbl}; |
4060 | + my $nibble_iter = $args{NibbleIterator}; |
4061 | + my $sth = $nibble_iter->statements(); |
4062 | + my $boundary = $nibble_iter->boundaries(); |
4063 | + |
4064 | + return if $o->get('dry-run'); |
4065 | + |
4066 | + # Count every chunk, even if it's ultimately skipped, etc. |
4067 | + $tbl->{results}->{n_chunks}++; |
4068 | + |
4069 | + # If the table is being chunk (i.e., it's not small enough to be |
4070 | + # consumed by one nibble), then check index usage and chunk size. |
4071 | + if ( !$nibble_iter->one_nibble() ) { |
4072 | + my $expl = explain_statement( |
4073 | + tbl => $tbl, |
4074 | + sth => $sth->{explain_nibble}, |
4075 | + vals => [ @{$boundary->{lower}}, @{$boundary->{upper}} ], |
4076 | + ); |
4077 | + |
4078 | + # Ensure that MySQL is using the chunk index. |
4079 | + if ( lc($expl->{key} || '') |
4080 | + ne lc($nibble_iter->nibble_index() || '') ) { |
4081 | + my $msg |
4082 | + = "Aborting copying table $tbl->{name} at chunk " |
4083 | + . $nibble_iter->nibble_number() |
4084 | + . " because it is not safe to chunk. Chunking should " |
4085 | + . "use the " |
4086 | + . ($nibble_iter->nibble_index() || '?') |
4087 | + . " index, but MySQL EXPLAIN reports that " |
4088 | + . ($expl->{key} ? "the $expl->{key}" : "no") |
4089 | + . " index will be used for " |
4090 | + . $sth->{explain_nibble}->{Statement} |
4091 | + . " with values " |
4092 | + . join(", ", map { defined $_ ? $_ : "NULL" } |
4093 | + (@{$boundary->{lower}}, @{$boundary->{upper}})) |
4094 | + . "\n"; |
4095 | + die $msg; |
4096 | + } |
4097 | + |
4098 | + # Check chunk size limit if the upper boundary and next lower |
4099 | + # boundary are identical. |
4100 | + if ( $limit ) { |
4101 | + my $boundary = $nibble_iter->boundaries(); |
4102 | + my $oversize_chunk |
4103 | + = $limit ? ($expl->{rows} || 0) >= $tbl->{chunk_size} * $limit |
4104 | + : 0; |
4105 | + if ( $oversize_chunk |
4106 | + && $nibble_iter->identical_boundaries( |
4107 | + $boundary->{upper}, $boundary->{next_lower}) ) |
4108 | + { |
4109 | + my $msg |
4110 | + = "Aborting copying table $tbl->{name} at chunk " |
4111 | + . $nibble_iter->nibble_number() |
4112 | + . " because the chunk is too large: MySQL estimates " |
4113 | + . ($expl->{rows} || 0) . "rows. The current chunk " |
4114 | + . "size limit is " . ($tbl->{chunk_size} * $limit) |
4115 | + . " rows (chunk size=$tbl->{chunk_size}" |
4116 | + . " * chunk size limit=$limit).\n"; |
4117 | + die $msg; |
4118 | + } |
4119 | + } |
4120 | + } |
4121 | + |
4122 | + # Exec and time the chunk checksum query. |
4123 | + $tbl->{nibble_time} = exec_nibble( |
4124 | + %args, |
4125 | + Retry => $retry, |
4126 | + Quoter => $q, |
4127 | + OptionParser => $o, |
4128 | + ); |
4129 | + PTDEBUG && _d('Nibble time:', $tbl->{nibble_time}); |
4130 | + |
4131 | + # We're executing REPLACE queries which don't return rows. |
4132 | + # Returning 0 from this callback causes the nibble iter to |
4133 | + # get the next boundaries/nibble. |
4134 | + return 0; |
4135 | + }, |
4136 | + after_nibble => sub { |
4137 | + my (%args) = @_; |
4138 | + my $tbl = $args{tbl}; |
4139 | + my $nibble_iter = $args{NibbleIterator}; |
4140 | + |
4141 | + return unless $o->get('execute'); |
4142 | + |
4143 | + # Update rate, chunk size, and progress if the nibble actually |
4144 | + # selected some rows. |
4145 | + my $cnt = $tbl->{row_cnt}; |
4146 | + if ( ($cnt || 0) > 0 ) { |
4147 | + # Update the rate of rows per second for the entire server. |
4148 | + # This is used for the initial chunk size of the next table. |
4149 | + $total_rows += $cnt; |
4150 | + $total_time += $tbl->{nibble_time}; |
4151 | + $avg_rate = int($total_rows / $total_time); |
4152 | + PTDEBUG && _d('Average copy rate (rows/s):', $avg_rate); |
4153 | + |
4154 | + # Adjust chunk size. This affects the next chunk. |
4155 | + if ( $chunk_time ) { |
4156 | + # Calcuate a new chunk-size based on the rate of rows/s. |
4157 | + $tbl->{chunk_size} = $tbl->{rate}->update( |
4158 | + $cnt, # processed this many rows |
4159 | + $tbl->{nibble_time}, # is this amount of time |
4160 | + ); |
4161 | + |
4162 | + if ( $tbl->{chunk_size} < 1 ) { |
4163 | + # This shouldn't happen. WeightedAvgRate::update() may |
4164 | + # return a value < 1, but minimum chunk size is 1. |
4165 | + $tbl->{chunk_size} = 1; |
4166 | + |
4167 | + # This warning is printed once per table. |
4168 | + if ( !$tbl->{warned_slow} ) { |
4169 | + warn "Rows are copying very slowly. " |
4170 | + . "--chunk-size has been automatically reduced to 1. " |
4171 | + . "Check that the server is not being overloaded, " |
4172 | + . "or increase --chunk-time. The last chunk " |
4173 | + . "selected $cnt rows and took " |
4174 | + . sprintf('%.3f', $tbl->{nibble_time}) |
4175 | + . " seconds to execute.\n"; |
4176 | + $tbl->{warned_slow} = 1; |
4177 | + } |
4178 | + } |
4179 | + |
4180 | + # Update chunk-size based on the rate of rows/s. |
4181 | + $nibble_iter->set_chunk_size($tbl->{chunk_size}); |
4182 | + } |
4183 | + |
4184 | + # Every table should have a Progress obj; update it. |
4185 | + if ( my $tbl_pr = $tbl->{progress} ) { |
4186 | + $tbl_pr->update( sub { return $total_rows } ); |
4187 | + } |
4188 | + } |
4189 | + |
4190 | + # Wait forever for slaves to catch up. |
4191 | + $replica_lag_pr->start() if $replica_lag_pr; |
4192 | + $replica_lag->wait(Progress => $replica_lag_pr); |
4193 | + |
4194 | + # Wait forever for system load to abate. wait() will die if |
4195 | + # --critical load is reached. |
4196 | + $sys_load_pr->start() if $sys_load_pr; |
4197 | + $sys_load->wait(Progress => $sys_load_pr); |
4198 | + |
4199 | + return; |
4200 | + }, |
4201 | + done => sub { |
4202 | + if ( $o->get('execute') ) { |
4203 | + print "Copied rows OK.\n"; |
4204 | + } |
4205 | + }, |
4206 | + }; |
4207 | + |
4208 | + # NibbleIterator combines these two statements and adds |
4209 | + # "FROM $orig_table->{name} WHERE <nibble stuff>". |
4210 | + my $dml = "INSERT LOW_PRIORITY IGNORE INTO $new_tbl->{name} " |
4211 | + . "(" . join(', ', map { $q->quote($_) } @common_cols) . ") " |
4212 | + . "SELECT"; |
4213 | + my $select = join(', ', map { $q->quote($_) } @common_cols); |
4214 | + |
4215 | + # The chunk size is auto-adjusted, so use --chunk-size as |
4216 | + # the initial value, but then save and update the adjusted |
4217 | + # chunk size in the table data struct. |
4218 | + $orig_tbl->{chunk_size} = $o->get('chunk-size'); |
4219 | + |
4220 | + # This won't (shouldn't) fail because we already verified in |
4221 | + # check_orig_table() table we can NibbleIterator::can_nibble(). |
4222 | + my $nibble_iter = new NibbleIterator( |
4223 | + Cxn => $cxn, |
4224 | + tbl => $orig_tbl, |
4225 | + chunk_size => $orig_tbl->{chunk_size}, |
4226 | + chunk_index => $o->get('chunk-index'), |
4227 | + dml => $dml, |
4228 | + select => $select, |
4229 | + callbacks => $callbacks, |
4230 | + OptionParser => $o, |
4231 | + Quoter => $q, |
4232 | + TableParser => $tp, |
4233 | + TableNibbler => new TableNibbler(TableParser => $tp, Quoter => $q), |
4234 | + comments => { |
4235 | + bite => "pt-online-schema-change $PID copy table", |
4236 | + nibble => "pt-online-schema-change $PID copy nibble", |
4237 | + }, |
4238 | + ); |
4239 | + |
4240 | + # Init a new weighted avg rate calculator for the table. |
4241 | + $orig_tbl->{rate} = new WeightedAvgRate(target_t => $chunk_time); |
4242 | + |
4243 | + # Make a Progress obj for this table. It may not be used; |
4244 | + # depends on how many rows, chunk size, how fast the server |
4245 | + # is, etc. But just in case, all tables have a Progress obj. |
4246 | + if ( $o->get('progress') |
4247 | + && !$nibble_iter->one_nibble() |
4248 | + && $nibble_iter->row_estimate() ) |
4249 | + { |
4250 | + $orig_tbl->{progress} = new Progress( |
4251 | + jobsize => $nibble_iter->row_estimate(), |
4252 | + spec => $o->get('progress'), |
4253 | + name => "Copying $orig_tbl->{name}", |
4254 | + ); |
4255 | + } |
4256 | + |
4257 | + # Start copying rows. This may take awhile, but --progress is on |
4258 | + # by default so there will be progress updates to stderr. |
4259 | + eval { |
4260 | + 1 while $nibble_iter->next(); |
4261 | + }; |
4262 | + if ( $EVAL_ERROR ) { |
4263 | + die "Error copying rows from $orig_tbl->{name} to " |
4264 | + . "$new_tbl->{name}: $EVAL_ERROR\n"; |
4265 | + } |
4266 | + $orig_tbl->{copied} = 1; # flag for cleanup tasks |
4267 | + |
4268 | + |
4269 | + # XXX Auto-choose the alter fk method BEFORE swapping/renaming tables |
4270 | + # else everything will break because if drop_swap is chosen, then we |
4271 | + # most NOT rename tables or drop the old table. |
4272 | + if ( $alter_fk_method eq 'auto' ) { |
4273 | + # If chunk time is set, then use the average rate of rows/s |
4274 | + # from copying the orig table to determine the max size of |
4275 | + # a child table that can be altered within one chunk time. |
4276 | + # The limit is a fudge factor. Chunk time won't be set if |
4277 | + # the user specified --chunk-size=N on the cmd line, in which |
4278 | + # case the max child table size is their specified chunk size |
4279 | + # times the fudge factor. |
4280 | + my $max_rows |
4281 | + = $o->get('dry-run') ? $o->get('chunk-size') * $limit |
4282 | + : $chunk_time ? $avg_rate * $chunk_time * $limit |
4283 | + : $o->get('chunk-size') * $limit; |
4284 | + PTDEBUG && _d('Max allowed child table size:', $max_rows); |
4285 | + |
4286 | + $alter_fk_method = determine_alter_fk_method( |
4287 | + child_tables => $child_tables, |
4288 | + max_rows => $max_rows, |
4289 | + Cxn => $cxn, |
4290 | + OptionParser => $o, |
4291 | + ); |
4292 | + |
4293 | + if ( $alter_fk_method eq 'drop_swap' ) { |
4294 | + $o->set('swap-tables', 0); |
4295 | + $o->set('drop-old-table', 0); |
4296 | + } |
4297 | + } |
4298 | + |
4299 | + # ##################################################################### |
4300 | + # XXX |
4301 | + # Step 5: Rename tables: orig -> old, new -> orig |
4302 | + # Past this step, the original table has been altered. This shouldn't |
4303 | + # fail, but if it does, the failure could be serious depending on what |
4304 | + # state the tables are left in. |
4305 | + # XXX |
4306 | + # ##################################################################### |
4307 | + my $old_tbl; |
4308 | + if ( $o->get('swap-tables') ) { |
4309 | + eval { |
4310 | + $old_tbl = swap_tables( |
4311 | + orig_tbl => $orig_tbl, |
4312 | + new_tbl => $new_tbl, |
4313 | + suffix => '_old', |
4314 | + Cxn => $cxn, |
4315 | + Quoter => $q, |
4316 | + OptionParser => $o, |
4317 | + ); |
4318 | + }; |
4319 | + if ( $EVAL_ERROR ) { |
4320 | + die "Error swapping the tables: $EVAL_ERROR\n" |
4321 | + . "Verify that the original table $orig_tbl->{name} has not " |
4322 | + . "been modified or renamed to the old table $old_tbl->{name}. " |
4323 | + . "Then drop the new table $new_tbl->{name} if it exists.\n"; |
4324 | + } |
4325 | + } |
4326 | + $orig_tbl->{swapped} = 1; # flag for cleanup tasks |
4327 | + PTDEBUG && _d('Old table:', Dumper($old_tbl)); |
4328 | + |
4329 | + # ##################################################################### |
4330 | + # Step 6: Update foreign key constraints if there are child tables. |
4331 | + # ##################################################################### |
4332 | + if ( $child_tables ) { |
4333 | + eval { |
4334 | + if ( $alter_fk_method eq 'none' ) { |
4335 | + print "Not updating foreign keys because " |
4336 | + . "--alter-foreign-keys-method=none. Foreign keys " |
4337 | + . "that reference the table will no longer work.\n"; |
4338 | + } |
4339 | + elsif ( $alter_fk_method eq 'rebuild_constraints' ) { |
4340 | + rebuild_constraints( |
4341 | + orig_tbl => $orig_tbl, |
4342 | + old_tbl => $old_tbl, |
4343 | + child_tables => $child_tables, |
4344 | + OptionParser => $o, |
4345 | + Quoter => $q, |
4346 | + Cxn => $cxn, |
4347 | + TableParser => $tp, |
4348 | + ); |
4349 | + } |
4350 | + elsif ( $alter_fk_method eq 'drop_swap' ) { |
4351 | + drop_swap( |
4352 | + orig_tbl => $orig_tbl, |
4353 | + new_tbl => $new_tbl, |
4354 | + Cxn => $cxn, |
4355 | + OptionParser => $o, |
4356 | + ); |
4357 | } |
4358 | else { |
4359 | - die "Invalid --update-foreign-keys-method value: $rename_fk_method"; |
4360 | - } |
4361 | - } |
4362 | - |
4363 | - # ##################################################################### |
4364 | - # Cleanup. |
4365 | - # ##################################################################### |
4366 | - msg("Calling " . (ref $copy_rows). "::cleanup()"); |
4367 | - $copy_rows->cleanup(%plugin_args); |
4368 | - |
4369 | - msg("Calling " . (ref $capture_sync) . "::cleanup()"); |
4370 | - $capture_sync->cleanup(%plugin_args); |
4371 | - |
4372 | - if ( $o->get('rename-tables') && $o->get('drop-old-table') ) { |
4373 | - $sql = "DROP TABLE IF EXISTS `$db`.`$old_tbl`"; |
4374 | - msg($sql); |
4375 | - $dbh->do($sql) unless $o->get('print'); |
4376 | - } |
4377 | - }; |
4378 | - if ( $EVAL_ERROR ) { |
4379 | - warn "An error occurred:\n\n$EVAL_ERROR\n" |
4380 | - . "Some triggers, temp tables, etc. may not have been removed. " |
4381 | - . "Run with --cleanup-and-exit to remove these items.\n"; |
4382 | - $exit_status = 1; |
4383 | - } |
4384 | - |
4385 | - msg("$PROGRAM_NAME ended, exit status $exit_status"); |
4386 | + # This should "never" happen because we check this var earlier. |
4387 | + die "Invalid --alter-foreign-keys-method: $alter_fk_method\n"; |
4388 | + } |
4389 | + }; |
4390 | + if ( $EVAL_ERROR ) { |
4391 | + # TODO: improve error message and handling. |
4392 | + die "Error updating foreign key constraints: $EVAL_ERROR\n"; |
4393 | + } |
4394 | + } |
4395 | + |
4396 | + # ######################################################################## |
4397 | + # Step 7: Drop the old table. |
4398 | + # ######################################################################## |
4399 | + if ( $o->get('drop-old-table') ) { |
4400 | + if ( $o->get('dry-run') ) { |
4401 | + print "Not dropping old table because this is a dry run.\n"; |
4402 | + } |
4403 | + else { |
4404 | + print "Dropping old table...\n"; |
4405 | + |
4406 | + if ( $alter_fk_method eq 'none' ) { |
4407 | + # Child tables still reference the old table, but the user |
4408 | + # has chosen to break fks, so we need to disable fk checks |
4409 | + # in order to drop the old table. |
4410 | + my $sql = "SET foreign_key_checks=0"; |
4411 | + PTDEBUG && _d($sql); |
4412 | + print $sql, "\n" if $o->get('print'); |
4413 | + $cxn->dbh()->do($sql); |
4414 | + } |
4415 | + |
4416 | + my $sql = "DROP TABLE IF EXISTS $old_tbl->{name}"; |
4417 | + print $sql, "\n" if $o->get('print'); |
4418 | + PTDEBUG && _d($sql); |
4419 | + eval { |
4420 | + $cxn->dbh()->do($sql); |
4421 | + }; |
4422 | + if ( $EVAL_ERROR ) { |
4423 | + die "Error dropping the old table: $EVAL_ERROR\n"; |
4424 | + } |
4425 | + print "Dropped old table $old_tbl->{name} OK.\n"; |
4426 | + } |
4427 | + } |
4428 | + |
4429 | + # ######################################################################## |
4430 | + # Done. |
4431 | + # ######################################################################## |
4432 | + $orig_tbl->{success} = 1; # flag for cleanup tasks |
4433 | + $cleanup = undef; # exec cleanup tasks |
4434 | + |
4435 | return $exit_status; |
4436 | } |
4437 | |
4438 | # ############################################################################ |
4439 | # Subroutines. |
4440 | # ############################################################################ |
4441 | -sub check_tables { |
4442 | +sub create_new_table{ |
4443 | + my (%args) = @_; |
4444 | + my @required_args = qw(orig_tbl Cxn Quoter OptionParser TableParser); |
4445 | + foreach my $arg ( @required_args ) { |
4446 | + die "I need a $arg argument" unless $args{$arg}; |
4447 | + } |
4448 | + my ($orig_tbl, $cxn, $q, $o, $tp) = @args{@required_args}; |
4449 | + |
4450 | + # Get the original table struct. |
4451 | + my $ddl = $tp->get_create_table( |
4452 | + $cxn->dbh(), |
4453 | + $orig_tbl->{db}, |
4454 | + $orig_tbl->{tbl}, |
4455 | + ); |
4456 | + |
4457 | + my $tries = $args{tries} || 10; # don't try forever |
4458 | + my $prefix = $args{prefix} || '_'; |
4459 | + my $suffix = $args{suffix} || '_new'; |
4460 | + my $table_name = $orig_tbl->{tbl} . $suffix; |
4461 | + |
4462 | + print "Creating new table...\n"; |
4463 | + my $tryno = 1; |
4464 | + my @old_tables; |
4465 | + while ( $tryno++ < $tries ) { |
4466 | + $table_name = $prefix . $table_name; |
4467 | + my $quoted = $q->quote($orig_tbl->{db}, $table_name); |
4468 | + |
4469 | + # Generate SQL to create the new table. We do not use CREATE TABLE LIKE |
4470 | + # because it doesn't preserve foreign key constraints. Here we need to |
4471 | + # rename the FK constraints, too. This is because FK constraints are |
4472 | + # internally stored as <database>.<constraint> and there cannot be |
4473 | + # duplicates. If we don't rename the constraints, then InnoDB will throw |
4474 | + # error 121 (duplicate key violation) when we try to execute the CREATE |
4475 | + # TABLE. TODO: this code isn't perfect. If we rename a constraint from |
4476 | + # foo to _foo and there is already a constraint with that name in this |
4477 | + # or another table, we can still have a collision. But if there are |
4478 | + # multiple FKs on this table, it's hard to know which one is causing the |
4479 | + # trouble. Should we generate random/UUID FK names or something instead? |
4480 | + my $sql = $ddl; |
4481 | + $sql =~ s/\ACREATE TABLE .*?\($/CREATE TABLE $quoted (/m; |
4482 | + $sql =~ s/^ CONSTRAINT `/ CONSTRAINT `_/gm; |
4483 | + PTDEBUG && _d($sql); |
4484 | + eval { |
4485 | + $cxn->dbh()->do($sql); |
4486 | + }; |
4487 | + if ( $EVAL_ERROR ) { |
4488 | + # Ignore this error because if multiple instances of the tool |
4489 | + # are running, or previous runs failed and weren't cleaned up, |
4490 | + # then there will be other similarly named tables with fewer |
4491 | + # leading prefix chars. Or, in rarer cases, the db just happens |
4492 | + # to have a similarly named table created by the user for other |
4493 | + # purposes. |
4494 | + if ( $EVAL_ERROR =~ m/table.+?already exists/i ) { |
4495 | + push @old_tables, $q->quote($orig_tbl->{db}, $table_name); |
4496 | + next; |
4497 | + } |
4498 | + |
4499 | + # Some other error happened. Let the caller catch it. |
4500 | + die $EVAL_ERROR; |
4501 | + } |
4502 | + print $sql, "\n" if $o->get('print'); # the sql that work |
4503 | + print "Created new table $orig_tbl->{db}.$table_name OK.\n"; |
4504 | + return { # success |
4505 | + db => $orig_tbl->{db}, |
4506 | + tbl => $table_name, |
4507 | + name => $q->quote($orig_tbl->{db}, $table_name), |
4508 | + }; |
4509 | + } |
4510 | + |
4511 | + die "Failed to find a unique new table name after $tries attemps. " |
4512 | + . "The following tables exist which may be left over from previous " |
4513 | + . "failed runs of the tool:\n" |
4514 | + . join("\n", map { " $_" } @old_tables) |
4515 | + . "\nExamine these tables and drop some or all of them if they are " |
4516 | + . "no longer need, then re-run the tool.\n"; |
4517 | +} |
4518 | + |
4519 | +sub swap_tables { |
4520 | + my (%args) = @_; |
4521 | + my @required_args = qw(orig_tbl new_tbl Cxn Quoter OptionParser); |
4522 | + foreach my $arg ( @required_args ) { |
4523 | + die "I need a $arg argument" unless $args{$arg}; |
4524 | + } |
4525 | + my ($orig_tbl, $new_tbl, $cxn, $q, $o) = @args{@required_args}; |
4526 | + |
4527 | + my $prefix = '_'; |
4528 | + my $table_name = $orig_tbl->{tbl} . ($args{suffix} || ''); |
4529 | + my $tries = 10; # don't try forever |
4530 | + |
4531 | + # This sub only works for --execute. Since the options are |
4532 | + # mutually exclusive and we return in the if case, the elsif |
4533 | + # is just a paranoid check because swapping the tables is one |
4534 | + # of the most sensitive/dangerous operations. |
4535 | + if ( $o->get('dry-run') ) { |
4536 | + print "Not swapping tables because this is a dry run.\n"; |
4537 | + |
4538 | + # A return value really isn't needed, but this trick allows |
4539 | + # rebuild_constraints() to parse and show the sql statements |
4540 | + # it would used. Otherwise, this has no effect. |
4541 | + return $orig_tbl; |
4542 | + } |
4543 | + elsif ( $o->get('execute') ) { |
4544 | + print "Swapping tables...\n"; |
4545 | + |
4546 | + while ( $tries-- ) { |
4547 | + $table_name = $prefix . $table_name; |
4548 | + my $sql = "RENAME TABLE $orig_tbl->{name} " |
4549 | + . "TO " . $q->quote($orig_tbl->{db}, $table_name) |
4550 | + . ", $new_tbl->{name} TO $orig_tbl->{name}"; |
4551 | + PTDEBUG && _d($sql); |
4552 | + eval { |
4553 | + $cxn->dbh()->do($sql); |
4554 | + }; |
4555 | + if ( $EVAL_ERROR ) { |
4556 | + # Ignore this error because if multiple instances of the tool |
4557 | + # are running, or previous runs failed and weren't cleaned up, |
4558 | + # then there will be other similarly named tables with fewer |
4559 | + # leading prefix chars. Or, in rarer cases, the db just happens |
4560 | + # to have a similarly named table created by the user for other |
4561 | + # purposes. |
4562 | + next if $EVAL_ERROR =~ m/table.+?already exists/i; |
4563 | + |
4564 | + # Some other error happened. Let caller catch it. |
4565 | + die $EVAL_ERROR; |
4566 | + } |
4567 | + print $sql, "\n" if $o->get('print'); |
4568 | + print "Swapped original and new tables OK.\n"; |
4569 | + return { # success |
4570 | + db => $orig_tbl->{db}, |
4571 | + tbl => $table_name, |
4572 | + name => $q->quote($orig_tbl->{db}, $table_name), |
4573 | + }; |
4574 | + } |
4575 | + |
4576 | + # This shouldn't happen. |
4577 | + # Here and in the attempt to find a new table name we probably ought to |
4578 | + # use --retries (and maybe a Retry object?) |
4579 | + die "Failed to find a unique old table name after serveral attempts.\n"; |
4580 | + } |
4581 | +} |
4582 | + |
4583 | +sub check_orig_table { |
4584 | my ( %args ) = @_; |
4585 | - my @required_args = qw(dbh db tbl tmp_tbl old_tbl VersionParser Quoter TableParser OptionParser TableChunker); |
4586 | + my @required_args = qw(orig_tbl Cxn TableParser OptionParser Quoter); |
4587 | foreach my $arg ( @required_args ) { |
4588 | die "I need a $arg argument" unless $args{$arg}; |
4589 | } |
4590 | - my ($dbh, $db, $tbl, $tmp_tbl, $old_tbl, $o, $tp) |
4591 | - = @args{qw(dbh db tbl tmp_tbl old_tbl OptionParser TableParser)}; |
4592 | - |
4593 | - msg("Checking if table $tbl can be altered"); |
4594 | - my %tbl_info; |
4595 | - my $sql = ""; |
4596 | - |
4597 | - # ######################################################################## |
4598 | - # Check MySQL. |
4599 | - # ######################################################################## |
4600 | - # Although triggers were introduced in 5.0.2, "Prior to MySQL 5.0.10, |
4601 | - # triggers cannot contain direct references to tables by name." |
4602 | - if ( !$args{VersionParser}->version_ge($dbh, '5.0.10') ) { |
4603 | - die "This tool requires MySQL 5.0.10 or newer\n"; |
4604 | - } |
4605 | - |
4606 | - # ######################################################################## |
4607 | - # Check the (original) table. |
4608 | - # ######################################################################## |
4609 | - # The table must exist of course. |
4610 | - if ( !$tp->check_table(dbh=>$dbh, db=>$db, tbl=>$tbl) ) { |
4611 | - die "Table $db.$tbl does not exist\n"; |
4612 | - } |
4613 | - |
4614 | - # There cannot be any triggers on the table. |
4615 | - $sql = "SHOW TRIGGERS FROM `$db` LIKE '$tbl'"; |
4616 | - msg($sql); |
4617 | + my ($orig_tbl, $cxn, $tp, $o, $q) = @args{@required_args}; |
4618 | + |
4619 | + my $dbh = $cxn->dbh(); |
4620 | + |
4621 | + # The original table must exist, of course. |
4622 | + if (!$tp->check_table(dbh=>$dbh,db=>$orig_tbl->{db},tbl=>$orig_tbl->{tbl})) { |
4623 | + die "The original table $orig_tbl->{name} does not exist.\n"; |
4624 | + } |
4625 | + |
4626 | + # There cannot be any triggers on the original table. |
4627 | + my $sql = 'SHOW TRIGGERS FROM ' . $q->quote($orig_tbl->{db}) |
4628 | + . ' LIKE ' . $q->literal_like($orig_tbl->{tbl}); |
4629 | + PTDEBUG && _d($sql); |
4630 | my $triggers = $dbh->selectall_arrayref($sql); |
4631 | if ( $triggers && @$triggers ) { |
4632 | - die "Table $db.$tbl has triggers. This tool needs to create " |
4633 | - . "its own triggers, so the table cannot already have triggers.\n"; |
4634 | + die "The table $orig_tbl->{name} has triggers. This tool " |
4635 | + . "needs to create its own triggers, so the table cannot " |
4636 | + . "already have triggers.\n"; |
4637 | } |
4638 | |
4639 | - # For now, we require that the old table has an exact-chunkable |
4640 | - # column (i.e. unique single-column). |
4641 | - $tbl_info{tbl_struct} = $tp->parse($tp->get_create_table($dbh, $db, $tbl)); |
4642 | - my ($exact, @chunkable_cols) = $args{TableChunker}->find_chunk_columns( |
4643 | - tbl_struct => $tbl_info{tbl_struct}, |
4644 | - exact => 1, |
4645 | + # Get the table struct. NibbleIterator needs this, and so do we. |
4646 | + my $ddl = $tp->get_create_table( |
4647 | + $cxn->dbh(), |
4648 | + $orig_tbl->{db}, |
4649 | + $orig_tbl->{tbl}, |
4650 | ); |
4651 | - if ( !$exact || !@chunkable_cols ) { |
4652 | - die "Table $db.$tbl cannot be chunked because it does not have " |
4653 | - . "a unique, single-column index\n"; |
4654 | - } |
4655 | - $tbl_info{chunk_column} = $chunkable_cols[0]->{column}; |
4656 | - $tbl_info{chunk_index} = $chunkable_cols[0]->{index}; |
4657 | - |
4658 | - # ######################################################################## |
4659 | - # Check the tmp table. |
4660 | - # ######################################################################## |
4661 | - # The tmp table should not exist if we're supposed to create it. |
4662 | - # Else, if user specifies --no-create-tmp-table, they should ensure |
4663 | - # that it exists. |
4664 | - if ( $o->get('create-tmp-table') |
4665 | - && $tp->check_table(dbh=>$dbh, db=>$db, tbl=>$tmp_tbl) ) { |
4666 | - die "Temporary table $db.$tmp_tbl exists which will prevent " |
4667 | - . "--create-tmp-table from creating the temporary table.\n"; |
4668 | - } |
4669 | - |
4670 | - # ######################################################################## |
4671 | - # Check the old table. |
4672 | - # ######################################################################## |
4673 | - # If we're going to rename the tables, which we do by default, then |
4674 | - # the old table cannot already exist. |
4675 | - if ( $o->get('rename-tables') |
4676 | - && $tp->check_table(dbh=>$dbh, db=>$db, tbl=>$old_tbl) ) { |
4677 | - die "Table $db.$old_tbl exists which will prevent $db.$tbl " |
4678 | - . "from being renamed to it. Table $db.$old_tbl could be from " |
4679 | - . "a previous run that failed. See --drop-old-table for more " |
4680 | - . "information.\n"; |
4681 | - } |
4682 | - |
4683 | - return %tbl_info; |
4684 | + $orig_tbl->{tbl_struct} = $tp->parse($ddl); |
4685 | + |
4686 | + # Must be able to nibble the original table (to copy rows to the new table). |
4687 | + eval { |
4688 | + NibbleIterator::can_nibble( |
4689 | + Cxn => $cxn, |
4690 | + tbl => $orig_tbl, |
4691 | + chunk_size => $o->get('chunk-size'), |
4692 | + chunk_indx => $o->get('chunk-index'), |
4693 | + OptionParser => $o, |
4694 | + TableParser => $tp, |
4695 | + ); |
4696 | + }; |
4697 | + if ( $EVAL_ERROR ) { |
4698 | + die "Cannot chunk the original table $orig_tbl->{name}: $EVAL_ERROR\n"; |
4699 | + } |
4700 | + |
4701 | + # Find a pk or unique index to use for the delete trigger. can_nibble() |
4702 | + # above returns an index, but NibbleIterator will use non-unique indexes, |
4703 | + # so we have to do this again here. |
4704 | + my $indexes = $orig_tbl->{tbl_struct}->{indexes}; # brevity |
4705 | + foreach my $index ( $tp->sort_indexes($orig_tbl->{tbl_struct}) ) { |
4706 | + if ( $index eq 'PRIMARY' || $indexes->{$index}->{is_unique} ) { |
4707 | + PTDEBUG && _d('Delete trigger index:', Dumper($index)); |
4708 | + $orig_tbl->{del_index} = $index; |
4709 | + last; |
4710 | + } |
4711 | + } |
4712 | + if ( !$orig_tbl->{del_index} ) { |
4713 | + die "The original table $orig_tbl->{name} does not have a PRIMARY KEY " |
4714 | + . "or a unique index which is required for the DELETE trigger.\n"; |
4715 | + } |
4716 | + |
4717 | + return; # success |
4718 | } |
4719 | |
4720 | -sub get_child_tables { |
4721 | +sub find_child_tables { |
4722 | my ( %args ) = @_; |
4723 | - my @required_args = qw(dbh db tbl Quoter); |
4724 | + my @required_args = qw(tbl Cxn Quoter); |
4725 | foreach my $arg ( @required_args ) { |
4726 | die "I need a $arg argument" unless $args{$arg}; |
4727 | } |
4728 | - my ($dbh, $db, $tbl, $q) = @args{@required_args}; |
4729 | - |
4730 | - my $sql = "SELECT table_name " |
4731 | + my ($tbl, $cxn, $q) = @args{@required_args}; |
4732 | + |
4733 | + PTDEBUG && _d('Finding child tables'); |
4734 | + |
4735 | + my $sql = "SELECT table_schema, table_name " |
4736 | . "FROM information_schema.key_column_usage " |
4737 | - . "WHERE constraint_schema='$db' AND referenced_table_name='$tbl'"; |
4738 | - PTDEBUG && _d($dbh, $sql); |
4739 | - my $child_tables; |
4740 | - eval { |
4741 | - $child_tables = $dbh->selectall_arrayref($sql); |
4742 | - }; |
4743 | - if ( $EVAL_ERROR ) { |
4744 | - die "Error executing query to check $tbl for child tables.\n\n" |
4745 | - . "Query: $sql\n\n" |
4746 | - . "Error: $EVAL_ERROR" |
4747 | - } |
4748 | - |
4749 | - PTDEBUG && _d("Child tables:", join(', ', map { $_->[0] } @$child_tables)); |
4750 | - return map { $_->[0] } @$child_tables; |
4751 | -} |
4752 | - |
4753 | -sub update_foreign_key_constraints { |
4754 | - my ( %args ) = @_; |
4755 | - my @required_args = qw(msg dbh db tbl old_tbl child_tables Quoter); |
4756 | - foreach my $arg ( @required_args ) { |
4757 | - die "I need a $arg argument" unless $args{$arg}; |
4758 | - } |
4759 | - my ($msg, $dbh, $db, $tbl, $old_tbl, $child_tables, $q) |
4760 | + . "WHERE constraint_schema='$tbl->{db}' " |
4761 | + . "AND referenced_table_name='$tbl->{tbl}'"; |
4762 | + PTDEBUG && _d($sql); |
4763 | + my $rows = $cxn->dbh()->selectall_arrayref($sql); |
4764 | + if ( !$rows || !@$rows ) { |
4765 | + PTDEBUG && _d('No child tables found'); |
4766 | + return; |
4767 | + } |
4768 | + |
4769 | + my @child_tables; |
4770 | + foreach my $row ( @$rows ) { |
4771 | + my $tbl = { |
4772 | + db => $row->[0], |
4773 | + tbl => $row->[1], |
4774 | + name => $q->quote(@$row), |
4775 | + }; |
4776 | + |
4777 | + # Get row estimates for each child table so we can give the user |
4778 | + # some input on choosing an --alter-foreign-keys-method if they |
4779 | + # don't use "auto". |
4780 | + my ($n_rows) = NibbleIterator::get_row_estimate( |
4781 | + Cxn => $cxn, |
4782 | + tbl => $tbl, |
4783 | + ); |
4784 | + $tbl->{row_est} = $n_rows; |
4785 | + |
4786 | + push @child_tables, $tbl; |
4787 | + } |
4788 | + |
4789 | + PTDEBUG && _d('Child tables:', Dumper(\@child_tables)); |
4790 | + return \@child_tables; |
4791 | +} |
4792 | + |
4793 | +sub determine_alter_fk_method { |
4794 | + my ( %args ) = @_; |
4795 | + my @required_args = qw(child_tables max_rows Cxn OptionParser); |
4796 | + foreach my $arg ( @required_args ) { |
4797 | + die "I need a $arg argument" unless $args{$arg}; |
4798 | + } |
4799 | + my ($child_tables, $max_rows, $cxn, $o) = @args{@required_args}; |
4800 | + |
4801 | + if ( $o->get('dry-run') ) { |
4802 | + print "Not determining the method to update foreign keys " |
4803 | + . "because this is a dry run.\n"; |
4804 | + return ''; # $alter_fk_method can't be undef |
4805 | + } |
4806 | + |
4807 | + # The rebuild_constraints method is the default becuase it's safer |
4808 | + # and doesn't cause the orig table to go missing for a moment. |
4809 | + my $method = 'rebuild_constraints'; |
4810 | + |
4811 | + print "Max rows for the rebuild_constraints method: $max_rows\n", |
4812 | + "Determining the method to update foreign keys...\n"; |
4813 | + foreach my $child_tbl ( @$child_tables ) { |
4814 | + print " $child_tbl->{name}: "; |
4815 | + my ($n_rows) = NibbleIterator::get_row_estimate( |
4816 | + Cxn => $cxn, |
4817 | + tbl => $child_tbl, |
4818 | + ); |
4819 | + if ( $n_rows > $max_rows ) { |
4820 | + print "too many rows: $n_rows; must use drop_swap\n"; |
4821 | + $method = 'drop_swap'; |
4822 | + last; |
4823 | + } |
4824 | + else { |
4825 | + print "$n_rows rows; can use rebuild_constraints\n"; |
4826 | + } |
4827 | + } |
4828 | + |
4829 | + return $method || ''; # $alter_fk_method can't be undef |
4830 | +} |
4831 | + |
4832 | +sub rebuild_constraints { |
4833 | + my ( %args ) = @_; |
4834 | + my @required_args = qw(orig_tbl old_tbl child_tables |
4835 | + Cxn Quoter OptionParser TableParser); |
4836 | + foreach my $arg ( @required_args ) { |
4837 | + die "I need a $arg argument" unless $args{$arg}; |
4838 | + } |
4839 | + my ($orig_tbl, $old_tbl, $child_tables, $cxn, $q, $o, $tp) |
4840 | = @args{@required_args}; |
4841 | |
4842 | - my $constraint = qr/^\s+(CONSTRAINT.+?REFERENCES `$old_tbl`.+)$/mo; |
4843 | + # MySQL has a "feature" where if the parent tbl is in the same db, |
4844 | + # then the child tbl ref is simply `parent_tbl`, but if the parent tbl |
4845 | + # is in another db, then the child tbl ref is `other_db`.`parent_tbl`. |
4846 | + # When we recreate the ref below, we use the db-qualified form, and |
4847 | + # MySQL will automatically trim the db if the tables are in the same db. |
4848 | + my $quoted_old_table = $q->quote($old_tbl->{tbl}); |
4849 | + my $constraint = qr/ |
4850 | + ^\s+ |
4851 | + ( |
4852 | + CONSTRAINT.+? |
4853 | + REFERENCES\s(?:$quoted_old_table|$old_tbl->{name}) |
4854 | + .+ |
4855 | + )$ |
4856 | + /xm; |
4857 | + PTDEBUG && _d('Rebuilding fk constraint matching', $constraint); |
4858 | + |
4859 | + if ( $o->get('dry-run') ) { |
4860 | + print "Not rebuilding foreign key constraints because this is a dry run.\n"; |
4861 | + } |
4862 | + else { |
4863 | + print "Rebuilding foreign key constraints...\n"; |
4864 | + } |
4865 | |
4866 | CHILD_TABLE: |
4867 | - foreach my $child_table ( @$child_tables ) { |
4868 | - my $sql = "SHOW CREATE TABLE `$db`.`$child_table`"; |
4869 | - PTDEBUG && _d($dbh, $sql); |
4870 | - $msg->($sql); |
4871 | - my $table_def; |
4872 | - eval { |
4873 | - $table_def = $dbh->selectrow_arrayref($sql)->[1]; |
4874 | - }; |
4875 | - if ( $EVAL_ERROR ) { |
4876 | - warn "Skipping child table $child_table: $EVAL_ERROR"; |
4877 | - next CHILD_TABLE; |
4878 | - } |
4879 | - |
4880 | + foreach my $child_tbl ( @$child_tables ) { |
4881 | + my $table_def = $tp->get_create_table( |
4882 | + $cxn->dbh(), |
4883 | + $child_tbl->{db}, |
4884 | + $child_tbl->{tbl}, |
4885 | + ); |
4886 | my @constraints = $table_def =~ m/$constraint/g; |
4887 | if ( !@constraints ) { |
4888 | - warn "Child table `$child_table` does not have any foreign key " |
4889 | - . "constraints referencing $old_tbl"; |
4890 | + warn "$child_tbl->{name} has no foreign key " |
4891 | + . "constraints referencing $old_tbl->{name}.\n"; |
4892 | next CHILD_TABLE; |
4893 | } |
4894 | |
4895 | + my @rebuilt_constraints; |
4896 | foreach my $constraint ( @constraints ) { |
4897 | - my ($fk_symbol) = $constraint =~ m/CONSTRAINT\s+(\S+)/; |
4898 | - $sql = "ALTER TABLE `$db`.`$child_table` DROP FOREIGN KEY $fk_symbol"; |
4899 | - $msg->($sql); |
4900 | - $dbh->do($sql) unless $args{print}; |
4901 | - |
4902 | - $constraint =~ s/REFERENCES `$old_tbl`/REFERENCES `$tbl`/o; |
4903 | - $sql = "ALTER TABLE `$db`.`$child_table` ADD $constraint"; |
4904 | - $msg->($sql); |
4905 | - $dbh->do($sql) unless $args{print}; |
4906 | - } |
4907 | - } |
4908 | - |
4909 | - return; |
4910 | -} |
4911 | - |
4912 | -sub intersection { |
4913 | - my ( $hashes ) = @_; |
4914 | - my %keys = map { $_ => 1 } keys %{$hashes->[0]}; |
4915 | - my $n_hashes = (scalar @$hashes) - 1; |
4916 | - my @isect = grep { $keys{$_} } map { keys %{$hashes->[$_]} } 1..$n_hashes; |
4917 | - return @isect; |
4918 | -} |
4919 | - |
4920 | -sub get_cxn { |
4921 | - my ( %args ) = @_; |
4922 | - my ($dsn, $ac, $dp, $o) = @args{qw(dsn AutoCommit DSNParser OptionParser)}; |
4923 | - |
4924 | - if ( $o->get('ask-pass') ) { |
4925 | - $dsn->{p} = OptionParser::prompt_noecho("Enter password: "); |
4926 | - } |
4927 | - my $dbh = $dp->get_dbh($dp->get_cxn_params($dsn), {AutoCommit => $ac}); |
4928 | - |
4929 | - $dbh->do('SET SQL_LOG_BIN=0') unless $o->get('bin-log'); |
4930 | - $dbh->do('SET FOREIGN_KEY_CHECKS=0') unless $o->get('foreign-key-checks'); |
4931 | - |
4932 | - return $dbh; |
4933 | -} |
4934 | - |
4935 | -sub msg { |
4936 | - my ( $msg ) = @_; |
4937 | - chomp $msg; |
4938 | - print '# ', ts(time), " $msg\n" unless $quiet; |
4939 | - PTDEBUG && _d($msg); |
4940 | - return; |
4941 | -} |
4942 | - |
4943 | -# Only for tests which may not call main(). |
4944 | -sub __set_quiet { |
4945 | - $quiet = $_[0]; |
4946 | + PTDEBUG && _d('Rebuilding fk constraint:', $constraint); |
4947 | + |
4948 | + # Remove trailing commas in case there are multiple constraints on the |
4949 | + # table. |
4950 | + $constraint =~ s/,$//; |
4951 | + |
4952 | + # Find the constraint name. It will be quoted already. |
4953 | + my ($fk) = $constraint =~ m/CONSTRAINT\s+`([^`]+)`/; |
4954 | + |
4955 | + # Drop the reference to the old table/renamed orig table, and add a new |
4956 | + # reference to the new table. InnoDB will throw an error if the new |
4957 | + # constraint has the same name as the old one, so we must rename it. |
4958 | + # Example: after renaming sakila.actor to sakila.actor_old (for |
4959 | + # example), the foreign key on film_actor looks like this: |
4960 | + # CONSTRAINT `fk_film_actor_actor` FOREIGN KEY (`actor_id`) REFERENCES |
4961 | + # `actor_old` (`actor_id`) ON UPDATE CASCADE |
4962 | + # We need it to look like this instead: |
4963 | + # CONSTRAINT `_fk_film_actor_actor` FOREIGN KEY (`actor_id`) REFERENCES |
4964 | + # `actor` (`actor_id`) ON UPDATE CASCADE |
4965 | + # Reference the correct table name... |
4966 | + $constraint =~ s/REFERENCES[^\(]+/REFERENCES $orig_tbl->{name} /; |
4967 | + # And rename the constraint to avoid conflict |
4968 | + $constraint =~ s/CONSTRAINT `$fk`/CONSTRAINT `_$fk`/; |
4969 | + |
4970 | + my $sql = "DROP FOREIGN KEY `$fk`, " |
4971 | + . "ADD $constraint"; |
4972 | + push @rebuilt_constraints, $sql; |
4973 | + } |
4974 | + |
4975 | + my $sql = "ALTER TABLE $child_tbl->{name} " |
4976 | + . join(', ', @rebuilt_constraints); |
4977 | + print $sql, "\n" if $o->get('print'); |
4978 | + if ( $o->get('execute') ) { |
4979 | + PTDEBUG && _d($sql); |
4980 | + $cxn->dbh()->do($sql); |
4981 | + } |
4982 | + } |
4983 | + |
4984 | + if ( $o->get('execute') ) { |
4985 | + print "Rebuilt foreign key constraints OK.\n"; |
4986 | + } |
4987 | + |
4988 | + return; |
4989 | +} |
4990 | + |
4991 | +sub drop_swap { |
4992 | + my ( %args ) = @_; |
4993 | + my @required_args = qw(orig_tbl new_tbl Cxn OptionParser); |
4994 | + foreach my $arg ( @required_args ) { |
4995 | + die "I need a $arg argument" unless $args{$arg}; |
4996 | + } |
4997 | + my ($orig_tbl, $new_tbl, $cxn, $o) = @args{@required_args}; |
4998 | + |
4999 | + if ( $o->get('dry-run') ) { |
5000 | + print "Not drop-swapping tables because this is a dry run.\n"; |
The diff has been truncated for viewing.