Merge lp:~percona-toolkit-dev/percona-toolkit/fix-table-status-bug-960513 into lp:percona-toolkit/2.0

Proposed by Daniel Nichter
Status: Rejected
Rejected by: Daniel Nichter
Proposed branch: lp:~percona-toolkit-dev/percona-toolkit/fix-table-status-bug-960513
Merge into: lp:percona-toolkit/2.0
Diff against target: 15458 lines (+13654/-776)
41 files modified
bin/pt-duplicate-key-checker (+171/-92)
bin/pt-fingerprint (+2143/-0)
bin/pt-index-usage (+170/-92)
bin/pt-table-checksum (+42/-67)
bin/pt-table-sync (+553/-404)
bin/pt-table-usage (+7320/-0)
lib/NibbleIterator.pm (+8/-15)
lib/QueryRewriter.pm (+24/-4)
lib/SQLParser.pm (+98/-8)
lib/SchemaIterator.pm (+27/-45)
lib/TableUsage.pm (+1060/-0)
t/lib/QueryRewriter.t (+59/-1)
t/lib/SchemaIterator.t (+2/-47)
t/lib/TableUsage.t (+817/-0)
t/lib/samples/SchemaIterator/all-dbs-tbls.txt (+486/-0)
t/lib/samples/SchemaIterator/resume-from-ignored-sakila-payment.txt (+49/-0)
t/lib/samples/SchemaIterator/resume-from-sakila-payment.txt (+66/-0)
t/pt-fingerprint/basics.t (+101/-0)
t/pt-fingerprint/samples/query001 (+2/-0)
t/pt-fingerprint/samples/query001.fingerprint (+1/-0)
t/pt-fingerprint/samples/query002 (+2/-0)
t/pt-fingerprint/samples/query002.fingerprint (+1/-0)
t/pt-table-sync/issue_408.t (+1/-1)
t/pt-table-usage/basics.t (+138/-0)
t/pt-table-usage/create_table_definitions.t (+41/-0)
t/pt-table-usage/explain_extended.t (+79/-0)
t/pt-table-usage/samples/ee.out (+6/-0)
t/pt-table-usage/samples/ee.sql (+26/-0)
t/pt-table-usage/samples/in/slow001.txt (+24/-0)
t/pt-table-usage/samples/in/slow002.txt (+20/-0)
t/pt-table-usage/samples/in/slow003.txt (+3/-0)
t/pt-table-usage/samples/out/create-table-defs-001.txt (+4/-0)
t/pt-table-usage/samples/out/create001.txt (+5/-0)
t/pt-table-usage/samples/out/drop-table-if-exists.txt (+3/-0)
t/pt-table-usage/samples/out/query001.txt (+6/-0)
t/pt-table-usage/samples/out/query002.txt (+5/-0)
t/pt-table-usage/samples/out/slow001.txt (+31/-0)
t/pt-table-usage/samples/out/slow002.txt (+40/-0)
t/pt-table-usage/samples/out/slow003-001.txt (+6/-0)
t/pt-table-usage/samples/out/slow003-002.txt (+8/-0)
t/pt-table-usage/samples/out/slow003-003.txt (+6/-0)
To merge this branch: bzr merge lp:~percona-toolkit-dev/percona-toolkit/fix-table-status-bug-960513
Reviewer Review Type Date Requested Status
Daniel Nichter Approve
Review via email: mp+100296@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Daniel Nichter (daniel-nichter) :
review: Approve

Unmerged revisions

228. By Daniel Nichter

Update SchemaIterator, TableParser, and NibbleIterator in tools that use them. All tools' tests still pass.

227. By Daniel Nichter

Don't use TABLE STATUS, use CREATE TABLE instead for the table's engine.

226. By Daniel Nichter

Update SchemaIterator in pt-table-sync. Make the module handle NAME_lc or not.

225. By Daniel Nichter

Add pt-table-usage and update SQLParser.pm from Maatkit.

224. By Daniel Nichter

Add pt-fingerprint.

223. By Daniel Nichter

Merge fix-no-stalk-bug-955860.

222. By Daniel Nichter

Merge fix-pt-upgrade-docs-bug-953461.

221. By Daniel Nichter

Merge fix-quoting-bug-967451.

220. By Daniel Nichter

Merge fix-risks-docs-bug-949653.

219. By Daniel Nichter

Merge lp:~percona-toolkit-dev/percona-toolkit/update-relay-log-space-limit-docs-fix-bug-949154.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'bin/pt-duplicate-key-checker'
--- bin/pt-duplicate-key-checker 2012-03-07 23:41:54 +0000
+++ bin/pt-duplicate-key-checker 2012-03-31 16:07:24 +0000
@@ -199,19 +199,58 @@
199 return bless $self, $class;199 return bless $self, $class;
200}200}
201201
202sub get_create_table {
203 my ( $self, $dbh, $db, $tbl ) = @_;
204 die "I need a dbh parameter" unless $dbh;
205 die "I need a db parameter" unless $db;
206 die "I need a tbl parameter" unless $tbl;
207 my $q = $self->{Quoter};
208
209 my $new_sql_mode
210 = '/*!40101 SET @OLD_SQL_MODE := @@SQL_MODE, '
211 . q{@@SQL_MODE := REPLACE(REPLACE(@@SQL_MODE, 'ANSI_QUOTES', ''), ',,', ','), }
212 . '@OLD_QUOTE := @@SQL_QUOTE_SHOW_CREATE, '
213 . '@@SQL_QUOTE_SHOW_CREATE := 1 */';
214
215 my $old_sql_mode = '/*!40101 SET @@SQL_MODE := @OLD_SQL_MODE, '
216 . '@@SQL_QUOTE_SHOW_CREATE := @OLD_QUOTE */';
217
218 PTDEBUG && _d($new_sql_mode);
219 eval { $dbh->do($new_sql_mode); };
220 PTDEBUG && $EVAL_ERROR && _d($EVAL_ERROR);
221
222 my $use_sql = 'USE ' . $q->quote($db);
223 PTDEBUG && _d($dbh, $use_sql);
224 $dbh->do($use_sql);
225
226 my $show_sql = "SHOW CREATE TABLE " . $q->quote($db, $tbl);
227 PTDEBUG && _d($show_sql);
228 my $href;
229 eval { $href = $dbh->selectrow_hashref($show_sql); };
230 if ( $EVAL_ERROR ) {
231 PTDEBUG && _d($EVAL_ERROR);
232
233 PTDEBUG && _d($old_sql_mode);
234 $dbh->do($old_sql_mode);
235
236 return;
237 }
238
239 PTDEBUG && _d($old_sql_mode);
240 $dbh->do($old_sql_mode);
241
242 my ($key) = grep { m/create (?:table|view)/i } keys %$href;
243 if ( !$key ) {
244 die "Error: no 'Create Table' or 'Create View' in result set from "
245 . "$show_sql: " . Dumper($href);
246 }
247
248 return $href->{$key};
249}
250
202sub parse {251sub parse {
203 my ( $self, $ddl, $opts ) = @_;252 my ( $self, $ddl, $opts ) = @_;
204 return unless $ddl;253 return unless $ddl;
205 if ( ref $ddl eq 'ARRAY' ) {
206 if ( lc $ddl->[0] eq 'table' ) {
207 $ddl = $ddl->[1];
208 }
209 else {
210 return {
211 engine => 'VIEW',
212 };
213 }
214 }
215254
216 if ( $ddl !~ m/CREATE (?:TEMPORARY )?TABLE `/ ) {255 if ( $ddl !~ m/CREATE (?:TEMPORARY )?TABLE `/ ) {
217 die "Cannot parse table definition; is ANSI quoting "256 die "Cannot parse table definition; is ANSI quoting "
@@ -518,41 +557,31 @@
518 return $ddl;557 return $ddl;
519}558}
520559
521sub remove_secondary_indexes {560sub get_table_status {
522 my ( $self, $ddl ) = @_;561 my ( $self, $dbh, $db, $like ) = @_;
523 my $sec_indexes_ddl;562 my $q = $self->{Quoter};
524 my $tbl_struct = $self->parse($ddl);563 my $sql = "SHOW TABLE STATUS FROM " . $q->quote($db);
525564 my @params;
526 if ( ($tbl_struct->{engine} || '') =~ m/InnoDB/i ) {565 if ( $like ) {
527 my $clustered_key = $tbl_struct->{clustered_key};566 $sql .= ' LIKE ?';
528 $clustered_key ||= '';567 push @params, $like;
529568 }
530 my @sec_indexes = map {569 PTDEBUG && _d($sql, @params);
531 my $key_def = $_->{ddl};570 my $sth = $dbh->prepare($sql);
532 $key_def =~ s/([\(\)])/\\$1/g;571 eval { $sth->execute(@params); };
533 $ddl =~ s/\s+$key_def//i;572 if ($EVAL_ERROR) {
534573 PTDEBUG && _d($EVAL_ERROR);
535 my $key_ddl = "ADD $_->{ddl}";574 return;
536 $key_ddl .= ',' unless $key_ddl =~ m/,$/;575 }
537 $key_ddl;576 my @tables = @{$sth->fetchall_arrayref({})};
538 }577 @tables = map {
539 grep { $_->{name} ne $clustered_key }578 my %tbl; # Make a copy with lowercased keys
540 values %{$tbl_struct->{keys}};579 @tbl{ map { lc $_ } keys %$_ } = values %$_;
541 PTDEBUG && _d('Secondary indexes:', Dumper(\@sec_indexes));580 $tbl{engine} ||= $tbl{type} || $tbl{comment};
542581 delete $tbl{type};
543 if ( @sec_indexes ) {582 \%tbl;
544 $sec_indexes_ddl = join(' ', @sec_indexes);583 } @tables;
545 $sec_indexes_ddl =~ s/,$//;584 return @tables;
546 }
547
548 $ddl =~ s/,(\n\) )/$1/s;
549 }
550 else {
551 PTDEBUG && _d('Not removing secondary indexes from',
552 $tbl_struct->{engine}, 'table');
553 }
554
555 return $ddl, $sec_indexes_ddl, $tbl_struct;
556}585}
557586
558sub _d {587sub _d {
@@ -3195,7 +3224,7 @@
31953224
3196sub new {3225sub new {
3197 my ( $class, %args ) = @_;3226 my ( $class, %args ) = @_;
3198 my @required_args = qw(OptionParser Quoter);3227 my @required_args = qw(OptionParser TableParser Quoter);
3199 foreach my $arg ( @required_args ) {3228 foreach my $arg ( @required_args ) {
3200 die "I need a $arg argument" unless $args{$arg};3229 die "I need a $arg argument" unless $args{$arg};
3201 }3230 }
@@ -3204,8 +3233,19 @@
3204 die "I need either a dbh or file_itr argument"3233 die "I need either a dbh or file_itr argument"
3205 if (!$dbh && !$file_itr) || ($dbh && $file_itr);3234 if (!$dbh && !$file_itr) || ($dbh && $file_itr);
32063235
3236 my %resume;
3237 if ( my $table = $args{resume} ) {
3238 PTDEBUG && _d('Will resume from or after', $table);
3239 my ($db, $tbl) = $args{Quoter}->split_unquote($table);
3240 die "Resume table must be database-qualified: $table"
3241 unless $db && $tbl;
3242 $resume{db} = $db;
3243 $resume{tbl} = $tbl;
3244 }
3245
3207 my $self = {3246 my $self = {
3208 %args,3247 %args,
3248 resume => \%resume,
3209 filters => _make_filters(%args),3249 filters => _make_filters(%args),
3210 };3250 };
32113251
@@ -3266,9 +3306,19 @@
3266 return \%filters;3306 return \%filters;
3267}3307}
32683308
3269sub next_schema_object {3309sub next {
3270 my ( $self ) = @_;3310 my ( $self ) = @_;
32713311
3312 if ( !$self->{initialized} ) {
3313 $self->{initialized} = 1;
3314 if ( $self->{resume}->{tbl}
3315 && !$self->table_is_allowed(@{$self->{resume}}{qw(db tbl)}) ) {
3316 PTDEBUG && _d('Will resume after',
3317 join('.', @{$self->{resume}}{qw(db tbl)}));
3318 $self->{resume}->{after} = 1;
3319 }
3320 }
3321
3272 my $schema_obj;3322 my $schema_obj;
3273 if ( $self->{file_itr} ) {3323 if ( $self->{file_itr} ) {
3274 $schema_obj= $self->_iterate_files();3324 $schema_obj= $self->_iterate_files();
@@ -3278,24 +3328,18 @@
3278 }3328 }
32793329
3280 if ( $schema_obj ) {3330 if ( $schema_obj ) {
3281 if ( $schema_obj->{ddl} && $self->{TableParser} ) {
3282 $schema_obj->{tbl_struct}
3283 = $self->{TableParser}->parse($schema_obj->{ddl});
3284 }
3285
3286 delete $schema_obj->{ddl} unless $self->{keep_ddl};
3287
3288 if ( my $schema = $self->{Schema} ) {3331 if ( my $schema = $self->{Schema} ) {
3289 $schema->add_schema_object($schema_obj);3332 $schema->add_schema_object($schema_obj);
3290 }3333 }
3334 PTDEBUG && _d('Next schema object:',
3335 $schema_obj->{db}, $schema_obj->{tbl});
3291 }3336 }
32923337
3293 PTDEBUG && _d('Next schema object:', $schema_obj->{db}, $schema_obj->{tbl});
3294 return $schema_obj;3338 return $schema_obj;
3295}3339}
32963340
3297sub _iterate_files {3341sub _iterate_files {
3298 my ( $self ) = @_;3342 my ( $self ) = @_;
32993343
3300 if ( !$self->{fh} ) {3344 if ( !$self->{fh} ) {
3301 my ($fh, $file) = $self->{file_itr}->();3345 my ($fh, $file) = $self->{file_itr}->();
@@ -3316,7 +3360,8 @@
3316 my $db = $1; # XXX3360 my $db = $1; # XXX
3317 $db =~ s/^`//; # strip leading `3361 $db =~ s/^`//; # strip leading `
3318 $db =~ s/`$//; # and trailing `3362 $db =~ s/`$//; # and trailing `
3319 if ( $self->database_is_allowed($db) ) {3363 if ( $self->database_is_allowed($db)
3364 && $self->_resume_from_database($db) ) {
3320 $self->{db} = $db;3365 $self->{db} = $db;
3321 }3366 }
3322 }3367 }
@@ -3329,21 +3374,22 @@
3329 my ($tbl) = $chunk =~ m/$tbl_name/;3374 my ($tbl) = $chunk =~ m/$tbl_name/;
3330 $tbl =~ s/^\s*`//;3375 $tbl =~ s/^\s*`//;
3331 $tbl =~ s/`\s*$//;3376 $tbl =~ s/`\s*$//;
3332 if ( $self->table_is_allowed($self->{db}, $tbl) ) {3377 if ( $self->_resume_from_table($tbl)
3378 && $self->table_is_allowed($self->{db}, $tbl) ) {
3333 my ($ddl) = $chunk =~ m/^(?:$open_comment)?(CREATE TABLE.+?;)$/ms;3379 my ($ddl) = $chunk =~ m/^(?:$open_comment)?(CREATE TABLE.+?;)$/ms;
3334 if ( !$ddl ) {3380 if ( !$ddl ) {
3335 warn "Failed to parse CREATE TABLE from\n" . $chunk;3381 warn "Failed to parse CREATE TABLE from\n" . $chunk;
3336 next CHUNK;3382 next CHUNK;
3337 }3383 }
3338 $ddl =~ s/ \*\/;\Z/;/; # remove end of version comment3384 $ddl =~ s/ \*\/;\Z/;/; # remove end of version comment
33393385 my $tbl_struct = $self->{TableParser}->parse($ddl);
3340 my ($engine) = $ddl =~ m/\).*?(?:ENGINE|TYPE)=(\w+)/; 3386 if ( $self->engine_is_allowed($tbl_struct->{engine}) ) {
3341
3342 if ( !$engine || $self->engine_is_allowed($engine) ) {
3343 return {3387 return {
3344 db => $self->{db},3388 db => $self->{db},
3345 tbl => $tbl,3389 tbl => $tbl,
3346 ddl => $ddl,3390 name => $self->{Quoter}->quote($self->{db}, $tbl),
3391 ddl => $ddl,
3392 tbl_struct => $tbl_struct,
3347 };3393 };
3348 }3394 }
3349 }3395 }
@@ -3360,6 +3406,7 @@
3360sub _iterate_dbh {3406sub _iterate_dbh {
3361 my ( $self ) = @_;3407 my ( $self ) = @_;
3362 my $q = $self->{Quoter};3408 my $q = $self->{Quoter};
3409 my $tp = $self->{TableParser};
3363 my $dbh = $self->{dbh};3410 my $dbh = $self->{dbh};
3364 PTDEBUG && _d('Getting next schema object from dbh', $dbh);3411 PTDEBUG && _d('Getting next schema object from dbh', $dbh);
33653412
@@ -3373,7 +3420,9 @@
3373 }3420 }
33743421
3375 if ( !$self->{db} ) {3422 if ( !$self->{db} ) {
3376 $self->{db} = shift @{$self->{dbs}};3423 do {
3424 $self->{db} = shift @{$self->{dbs}};
3425 } until $self->_resume_from_database($self->{db});
3377 PTDEBUG && _d('Next database:', $self->{db});3426 PTDEBUG && _d('Next database:', $self->{db});
3378 return unless $self->{db};3427 return unless $self->{db};
3379 }3428 }
@@ -3386,8 +3435,9 @@
3386 }3435 }
3387 grep {3436 grep {
3388 my ($tbl, $type) = @$_;3437 my ($tbl, $type) = @$_;
3389 $self->table_is_allowed($self->{db}, $tbl)3438 (!$type || ($type ne 'VIEW'))
3390 && (!$type || ($type ne 'VIEW'));3439 && $self->_resume_from_table($tbl)
3440 && $self->table_is_allowed($self->{db}, $tbl);
3391 }3441 }
3392 @{$dbh->selectall_arrayref($sql)};3442 @{$dbh->selectall_arrayref($sql)};
3393 PTDEBUG && _d('Found', scalar @tbls, 'tables in database', $self->{db});3443 PTDEBUG && _d('Found', scalar @tbls, 'tables in database', $self->{db});
@@ -3395,27 +3445,15 @@
3395 }3445 }
33963446
3397 while ( my $tbl = shift @{$self->{tbls}} ) {3447 while ( my $tbl = shift @{$self->{tbls}} ) {
3398 my $engine;3448 my $ddl = $tp->get_create_table($dbh, $self->{db}, $tbl);
3399 if ( $self->{filters}->{'engines'}3449 my $tbl_struct = $tp->parse($ddl);
3400 || $self->{filters}->{'ignore-engines'} ) {3450 if ( $self->engine_is_allowed($tbl_struct->{engine}) ) {
3401 my $sql = "SHOW TABLE STATUS FROM " . $q->quote($self->{db})
3402 . " LIKE \'$tbl\'";
3403 PTDEBUG && _d($sql);
3404 $engine = $dbh->selectrow_hashref($sql)->{engine};
3405 PTDEBUG && _d($tbl, 'uses', $engine, 'engine');
3406 }
3407
3408
3409 if ( !$engine || $self->engine_is_allowed($engine) ) {
3410 my $ddl;
3411 if ( my $du = $self->{MySQLDump} ) {
3412 $ddl = $du->get_create_table($dbh, $q, $self->{db}, $tbl)->[1];
3413 }
3414
3415 return {3451 return {
3416 db => $self->{db},3452 db => $self->{db},
3417 tbl => $tbl,3453 tbl => $tbl,
3418 ddl => $ddl,3454 name => $q->quote($self->{db}, $tbl),
3455 ddl => $ddl,
3456 tbl_struct => $tbl_struct,
3419 };3457 };
3420 }3458 }
3421 }3459 }
@@ -3476,6 +3514,10 @@
34763514
3477 my $filter = $self->{filters};3515 my $filter = $self->{filters};
34783516
3517 if ( $db eq 'mysql' && ($tbl eq 'general_log' || $tbl eq 'slow_log') ) {
3518 return 0;
3519 }
3520
3479 if ( $filter->{'ignore-tables'}->{$tbl}3521 if ( $filter->{'ignore-tables'}->{$tbl}
3480 && ($filter->{'ignore-tables'}->{$tbl} eq '*'3522 && ($filter->{'ignore-tables'}->{$tbl} eq '*'
3481 || $filter->{'ignore-tables'}->{$tbl} eq $db) ) {3523 || $filter->{'ignore-tables'}->{$tbl} eq $db) ) {
@@ -3515,7 +3557,11 @@
35153557
3516sub engine_is_allowed {3558sub engine_is_allowed {
3517 my ( $self, $engine ) = @_;3559 my ( $self, $engine ) = @_;
3518 die "I need an engine argument" unless $engine;3560
3561 if ( !$engine ) {
3562 PTDEBUG && _d('No engine specified; allowing the table');
3563 return 1;
3564 }
35193565
3520 $engine = lc $engine;3566 $engine = lc $engine;
35213567
@@ -3535,6 +3581,40 @@
3535 return 1;3581 return 1;
3536}3582}
35373583
3584sub _resume_from_database {
3585 my ($self, $db) = @_;
3586
3587 return 1 unless $self->{resume}->{db};
3588
3589 if ( $db eq $self->{resume}->{db} ) {
3590 PTDEBUG && _d('At resume db', $db);
3591 delete $self->{resume}->{db};
3592 return 1;
3593 }
3594
3595 return 0;
3596}
3597
3598sub _resume_from_table {
3599 my ($self, $tbl) = @_;
3600
3601 return 1 unless $self->{resume}->{tbl};
3602
3603 if ( $tbl eq $self->{resume}->{tbl} ) {
3604 if ( !$self->{resume}->{after} ) {
3605 PTDEBUG && _d('Resuming from table', $tbl);
3606 delete $self->{resume}->{tbl};
3607 return 1;
3608 }
3609 else {
3610 PTDEBUG && _d('Resuming after table', $tbl);
3611 delete $self->{resume}->{tbl};
3612 }
3613 }
3614
3615 return 0;
3616}
3617
3538sub _d {3618sub _d {
3539 my ($package, undef, $line) = caller 0;3619 my ($package, undef, $line) = caller 0;
3540 @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; }3620 @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; }
@@ -3644,11 +3724,10 @@
3644 MySQLDump => $du,3724 MySQLDump => $du,
3645 TableParser => $tp,3725 TableParser => $tp,
3646 Schema => $schema,3726 Schema => $schema,
3647 keep_ddl => 1,
3648 );3727 );
3649 TABLE:3728 TABLE:
3650 while ( my $tbl = $schema_itr->next_schema_object() ) {3729 while ( my $tbl = $schema_itr->next() ) {
3651 $tbl->{engine} = $tp->get_engine($tbl->{ddl});3730 $tbl->{engine} = $tbl->{tbl_struct}->{engine};
36523731
3653 my ($keys, $clustered_key, $fks);3732 my ($keys, $clustered_key, $fks);
3654 if ( $get_keys ) {3733 if ( $get_keys ) {
36553734
=== added file 'bin/pt-fingerprint'
--- bin/pt-fingerprint 1970-01-01 00:00:00 +0000
+++ bin/pt-fingerprint 2012-03-31 16:07:24 +0000
@@ -0,0 +1,2143 @@
1#!/usr/bin/env perl
2
3# This program is part of Percona Toolkit: http://www.percona.com/software/
4# See "COPYRIGHT, LICENSE, AND WARRANTY" at the end of this file for legal
5# notices and disclaimers.
6
7use strict;
8use warnings FATAL => 'all';
9use constant MKDEBUG => $ENV{MKDEBUG} || 0;
10
11# ###########################################################################
12# OptionParser package
13# This package is a copy without comments from the original. The original
14# with comments and its test file can be found in the Bazaar repository at,
15# lib/OptionParser.pm
16# t/lib/OptionParser.t
17# See https://launchpad.net/percona-toolkit for more information.
18# ###########################################################################
19{
20package OptionParser;
21
22use strict;
23use warnings FATAL => 'all';
24use English qw(-no_match_vars);
25use constant PTDEBUG => $ENV{PTDEBUG} || 0;
26
27use List::Util qw(max);
28use Getopt::Long;
29
30my $POD_link_re = '[LC]<"?([^">]+)"?>';
31
32sub new {
33 my ( $class, %args ) = @_;
34 my @required_args = qw();
35 foreach my $arg ( @required_args ) {
36 die "I need a $arg argument" unless $args{$arg};
37 }
38
39 my ($program_name) = $PROGRAM_NAME =~ m/([.A-Za-z-]+)$/;
40 $program_name ||= $PROGRAM_NAME;
41 my $home = $ENV{HOME} || $ENV{HOMEPATH} || $ENV{USERPROFILE} || '.';
42
43 my %attributes = (
44 'type' => 1,
45 'short form' => 1,
46 'group' => 1,
47 'default' => 1,
48 'cumulative' => 1,
49 'negatable' => 1,
50 );
51
52 my $self = {
53 head1 => 'OPTIONS', # These args are used internally
54 skip_rules => 0, # to instantiate another Option-
55 item => '--(.*)', # Parser obj that parses the
56 attributes => \%attributes, # DSN OPTIONS section. Tools
57 parse_attributes => \&_parse_attribs, # don't tinker with these args.
58
59 %args,
60
61 strict => 1, # disabled by a special rule
62 program_name => $program_name,
63 opts => {},
64 got_opts => 0,
65 short_opts => {},
66 defaults => {},
67 groups => {},
68 allowed_groups => {},
69 errors => [],
70 rules => [], # desc of rules for --help
71 mutex => [], # rule: opts are mutually exclusive
72 atleast1 => [], # rule: at least one opt is required
73 disables => {}, # rule: opt disables other opts
74 defaults_to => {}, # rule: opt defaults to value of other opt
75 DSNParser => undef,
76 default_files => [
77 "/etc/percona-toolkit/percona-toolkit.conf",
78 "/etc/percona-toolkit/$program_name.conf",
79 "$home/.percona-toolkit.conf",
80 "$home/.$program_name.conf",
81 ],
82 types => {
83 string => 's', # standard Getopt type
84 int => 'i', # standard Getopt type
85 float => 'f', # standard Getopt type
86 Hash => 'H', # hash, formed from a comma-separated list
87 hash => 'h', # hash as above, but only if a value is given
88 Array => 'A', # array, similar to Hash
89 array => 'a', # array, similar to hash
90 DSN => 'd', # DSN
91 size => 'z', # size with kMG suffix (powers of 2^10)
92 time => 'm', # time, with an optional suffix of s/h/m/d
93 },
94 };
95
96 return bless $self, $class;
97}
98
99sub get_specs {
100 my ( $self, $file ) = @_;
101 $file ||= $self->{file} || __FILE__;
102 my @specs = $self->_pod_to_specs($file);
103 $self->_parse_specs(@specs);
104
105 open my $fh, "<", $file or die "Cannot open $file: $OS_ERROR";
106 my $contents = do { local $/ = undef; <$fh> };
107 close $fh;
108 if ( $contents =~ m/^=head1 DSN OPTIONS/m ) {
109 PTDEBUG && _d('Parsing DSN OPTIONS');
110 my $dsn_attribs = {
111 dsn => 1,
112 copy => 1,
113 };
114 my $parse_dsn_attribs = sub {
115 my ( $self, $option, $attribs ) = @_;
116 map {
117 my $val = $attribs->{$_};
118 if ( $val ) {
119 $val = $val eq 'yes' ? 1
120 : $val eq 'no' ? 0
121 : $val;
122 $attribs->{$_} = $val;
123 }
124 } keys %$attribs;
125 return {
126 key => $option,
127 %$attribs,
128 };
129 };
130 my $dsn_o = new OptionParser(
131 description => 'DSN OPTIONS',
132 head1 => 'DSN OPTIONS',
133 dsn => 0, # XXX don't infinitely recurse!
134 item => '\* (.)', # key opts are a single character
135 skip_rules => 1, # no rules before opts
136 attributes => $dsn_attribs,
137 parse_attributes => $parse_dsn_attribs,
138 );
139 my @dsn_opts = map {
140 my $opts = {
141 key => $_->{spec}->{key},
142 dsn => $_->{spec}->{dsn},
143 copy => $_->{spec}->{copy},
144 desc => $_->{desc},
145 };
146 $opts;
147 } $dsn_o->_pod_to_specs($file);
148 $self->{DSNParser} = DSNParser->new(opts => \@dsn_opts);
149 }
150
151 if ( $contents =~ m/^=head1 VERSION\n\n^(.+)$/m ) {
152 $self->{version} = $1;
153 PTDEBUG && _d($self->{version});
154 }
155
156 return;
157}
158
159sub DSNParser {
160 my ( $self ) = @_;
161 return $self->{DSNParser};
162};
163
164sub get_defaults_files {
165 my ( $self ) = @_;
166 return @{$self->{default_files}};
167}
168
169sub _pod_to_specs {
170 my ( $self, $file ) = @_;
171 $file ||= $self->{file} || __FILE__;
172 open my $fh, '<', $file or die "Cannot open $file: $OS_ERROR";
173
174 my @specs = ();
175 my @rules = ();
176 my $para;
177
178 local $INPUT_RECORD_SEPARATOR = '';
179 while ( $para = <$fh> ) {
180 next unless $para =~ m/^=head1 $self->{head1}/;
181 last;
182 }
183
184 while ( $para = <$fh> ) {
185 last if $para =~ m/^=over/;
186 next if $self->{skip_rules};
187 chomp $para;
188 $para =~ s/\s+/ /g;
189 $para =~ s/$POD_link_re/$1/go;
190 PTDEBUG && _d('Option rule:', $para);
191 push @rules, $para;
192 }
193
194 die "POD has no $self->{head1} section" unless $para;
195
196 do {
197 if ( my ($option) = $para =~ m/^=item $self->{item}/ ) {
198 chomp $para;
199 PTDEBUG && _d($para);
200 my %attribs;
201
202 $para = <$fh>; # read next paragraph, possibly attributes
203
204 if ( $para =~ m/: / ) { # attributes
205 $para =~ s/\s+\Z//g;
206 %attribs = map {
207 my ( $attrib, $val) = split(/: /, $_);
208 die "Unrecognized attribute for --$option: $attrib"
209 unless $self->{attributes}->{$attrib};
210 ($attrib, $val);
211 } split(/; /, $para);
212 if ( $attribs{'short form'} ) {
213 $attribs{'short form'} =~ s/-//;
214 }
215 $para = <$fh>; # read next paragraph, probably short help desc
216 }
217 else {
218 PTDEBUG && _d('Option has no attributes');
219 }
220
221 $para =~ s/\s+\Z//g;
222 $para =~ s/\s+/ /g;
223 $para =~ s/$POD_link_re/$1/go;
224
225 $para =~ s/\.(?:\n.*| [A-Z].*|\Z)//s;
226 PTDEBUG && _d('Short help:', $para);
227
228 die "No description after option spec $option" if $para =~ m/^=item/;
229
230 if ( my ($base_option) = $option =~ m/^\[no\](.*)/ ) {
231 $option = $base_option;
232 $attribs{'negatable'} = 1;
233 }
234
235 push @specs, {
236 spec => $self->{parse_attributes}->($self, $option, \%attribs),
237 desc => $para
238 . (defined $attribs{default} ? " (default $attribs{default})" : ''),
239 group => ($attribs{'group'} ? $attribs{'group'} : 'default'),
240 };
241 }
242 while ( $para = <$fh> ) {
243 last unless $para;
244 if ( $para =~ m/^=head1/ ) {
245 $para = undef; # Can't 'last' out of a do {} block.
246 last;
247 }
248 last if $para =~ m/^=item /;
249 }
250 } while ( $para );
251
252 die "No valid specs in $self->{head1}" unless @specs;
253
254 close $fh;
255 return @specs, @rules;
256}
257
258sub _parse_specs {
259 my ( $self, @specs ) = @_;
260 my %disables; # special rule that requires deferred checking
261
262 foreach my $opt ( @specs ) {
263 if ( ref $opt ) { # It's an option spec, not a rule.
264 PTDEBUG && _d('Parsing opt spec:',
265 map { ($_, '=>', $opt->{$_}) } keys %$opt);
266
267 my ( $long, $short ) = $opt->{spec} =~ m/^([\w-]+)(?:\|([^!+=]*))?/;
268 if ( !$long ) {
269 die "Cannot parse long option from spec $opt->{spec}";
270 }
271 $opt->{long} = $long;
272
273 die "Duplicate long option --$long" if exists $self->{opts}->{$long};
274 $self->{opts}->{$long} = $opt;
275
276 if ( length $long == 1 ) {
277 PTDEBUG && _d('Long opt', $long, 'looks like short opt');
278 $self->{short_opts}->{$long} = $long;
279 }
280
281 if ( $short ) {
282 die "Duplicate short option -$short"
283 if exists $self->{short_opts}->{$short};
284 $self->{short_opts}->{$short} = $long;
285 $opt->{short} = $short;
286 }
287 else {
288 $opt->{short} = undef;
289 }
290
291 $opt->{is_negatable} = $opt->{spec} =~ m/!/ ? 1 : 0;
292 $opt->{is_cumulative} = $opt->{spec} =~ m/\+/ ? 1 : 0;
293 $opt->{is_required} = $opt->{desc} =~ m/required/ ? 1 : 0;
294
295 $opt->{group} ||= 'default';
296 $self->{groups}->{ $opt->{group} }->{$long} = 1;
297
298 $opt->{value} = undef;
299 $opt->{got} = 0;
300
301 my ( $type ) = $opt->{spec} =~ m/=(.)/;
302 $opt->{type} = $type;
303 PTDEBUG && _d($long, 'type:', $type);
304
305
306 $opt->{spec} =~ s/=./=s/ if ( $type && $type =~ m/[HhAadzm]/ );
307
308 if ( (my ($def) = $opt->{desc} =~ m/default\b(?: ([^)]+))?/) ) {
309 $self->{defaults}->{$long} = defined $def ? $def : 1;
310 PTDEBUG && _d($long, 'default:', $def);
311 }
312
313 if ( $long eq 'config' ) {
314 $self->{defaults}->{$long} = join(',', $self->get_defaults_files());
315 }
316
317 if ( (my ($dis) = $opt->{desc} =~ m/(disables .*)/) ) {
318 $disables{$long} = $dis;
319 PTDEBUG && _d('Deferring check of disables rule for', $opt, $dis);
320 }
321
322 $self->{opts}->{$long} = $opt;
323 }
324 else { # It's an option rule, not a spec.
325 PTDEBUG && _d('Parsing rule:', $opt);
326 push @{$self->{rules}}, $opt;
327 my @participants = $self->_get_participants($opt);
328 my $rule_ok = 0;
329
330 if ( $opt =~ m/mutually exclusive|one and only one/ ) {
331 $rule_ok = 1;
332 push @{$self->{mutex}}, \@participants;
333 PTDEBUG && _d(@participants, 'are mutually exclusive');
334 }
335 if ( $opt =~ m/at least one|one and only one/ ) {
336 $rule_ok = 1;
337 push @{$self->{atleast1}}, \@participants;
338 PTDEBUG && _d(@participants, 'require at least one');
339 }
340 if ( $opt =~ m/default to/ ) {
341 $rule_ok = 1;
342 $self->{defaults_to}->{$participants[0]} = $participants[1];
343 PTDEBUG && _d($participants[0], 'defaults to', $participants[1]);
344 }
345 if ( $opt =~ m/restricted to option groups/ ) {
346 $rule_ok = 1;
347 my ($groups) = $opt =~ m/groups ([\w\s\,]+)/;
348 my @groups = split(',', $groups);
349 %{$self->{allowed_groups}->{$participants[0]}} = map {
350 s/\s+//;
351 $_ => 1;
352 } @groups;
353 }
354 if( $opt =~ m/accepts additional command-line arguments/ ) {
355 $rule_ok = 1;
356 $self->{strict} = 0;
357 PTDEBUG && _d("Strict mode disabled by rule");
358 }
359
360 die "Unrecognized option rule: $opt" unless $rule_ok;
361 }
362 }
363
364 foreach my $long ( keys %disables ) {
365 my @participants = $self->_get_participants($disables{$long});
366 $self->{disables}->{$long} = \@participants;
367 PTDEBUG && _d('Option', $long, 'disables', @participants);
368 }
369
370 return;
371}
372
373sub _get_participants {
374 my ( $self, $str ) = @_;
375 my @participants;
376 foreach my $long ( $str =~ m/--(?:\[no\])?([\w-]+)/g ) {
377 die "Option --$long does not exist while processing rule $str"
378 unless exists $self->{opts}->{$long};
379 push @participants, $long;
380 }
381 PTDEBUG && _d('Participants for', $str, ':', @participants);
382 return @participants;
383}
384
385sub opts {
386 my ( $self ) = @_;
387 my %opts = %{$self->{opts}};
388 return %opts;
389}
390
391sub short_opts {
392 my ( $self ) = @_;
393 my %short_opts = %{$self->{short_opts}};
394 return %short_opts;
395}
396
397sub set_defaults {
398 my ( $self, %defaults ) = @_;
399 $self->{defaults} = {};
400 foreach my $long ( keys %defaults ) {
401 die "Cannot set default for nonexistent option $long"
402 unless exists $self->{opts}->{$long};
403 $self->{defaults}->{$long} = $defaults{$long};
404 PTDEBUG && _d('Default val for', $long, ':', $defaults{$long});
405 }
406 return;
407}
408
409sub get_defaults {
410 my ( $self ) = @_;
411 return $self->{defaults};
412}
413
414sub get_groups {
415 my ( $self ) = @_;
416 return $self->{groups};
417}
418
419sub _set_option {
420 my ( $self, $opt, $val ) = @_;
421 my $long = exists $self->{opts}->{$opt} ? $opt
422 : exists $self->{short_opts}->{$opt} ? $self->{short_opts}->{$opt}
423 : die "Getopt::Long gave a nonexistent option: $opt";
424
425 $opt = $self->{opts}->{$long};
426 if ( $opt->{is_cumulative} ) {
427 $opt->{value}++;
428 }
429 else {
430 $opt->{value} = $val;
431 }
432 $opt->{got} = 1;
433 PTDEBUG && _d('Got option', $long, '=', $val);
434}
435
436sub get_opts {
437 my ( $self ) = @_;
438
439 foreach my $long ( keys %{$self->{opts}} ) {
440 $self->{opts}->{$long}->{got} = 0;
441 $self->{opts}->{$long}->{value}
442 = exists $self->{defaults}->{$long} ? $self->{defaults}->{$long}
443 : $self->{opts}->{$long}->{is_cumulative} ? 0
444 : undef;
445 }
446 $self->{got_opts} = 0;
447
448 $self->{errors} = [];
449
450 if ( @ARGV && $ARGV[0] eq "--config" ) {
451 shift @ARGV;
452 $self->_set_option('config', shift @ARGV);
453 }
454 if ( $self->has('config') ) {
455 my @extra_args;
456 foreach my $filename ( split(',', $self->get('config')) ) {
457 eval {
458 push @extra_args, $self->_read_config_file($filename);
459 };
460 if ( $EVAL_ERROR ) {
461 if ( $self->got('config') ) {
462 die $EVAL_ERROR;
463 }
464 elsif ( PTDEBUG ) {
465 _d($EVAL_ERROR);
466 }
467 }
468 }
469 unshift @ARGV, @extra_args;
470 }
471
472 Getopt::Long::Configure('no_ignore_case', 'bundling');
473 GetOptions(
474 map { $_->{spec} => sub { $self->_set_option(@_); } }
475 grep { $_->{long} ne 'config' } # --config is handled specially above.
476 values %{$self->{opts}}
477 ) or $self->save_error('Error parsing options');
478
479 if ( exists $self->{opts}->{version} && $self->{opts}->{version}->{got} ) {
480 if ( $self->{version} ) {
481 print $self->{version}, "\n";
482 }
483 else {
484 print "Error parsing version. See the VERSION section of the tool's documentation.\n";
485 }
486 exit 0;
487 }
488
489 if ( @ARGV && $self->{strict} ) {
490 $self->save_error("Unrecognized command-line options @ARGV");
491 }
492
493 foreach my $mutex ( @{$self->{mutex}} ) {
494 my @set = grep { $self->{opts}->{$_}->{got} } @$mutex;
495 if ( @set > 1 ) {
496 my $err = join(', ', map { "--$self->{opts}->{$_}->{long}" }
497 @{$mutex}[ 0 .. scalar(@$mutex) - 2] )
498 . ' and --'.$self->{opts}->{$mutex->[-1]}->{long}
499 . ' are mutually exclusive.';
500 $self->save_error($err);
501 }
502 }
503
504 foreach my $required ( @{$self->{atleast1}} ) {
505 my @set = grep { $self->{opts}->{$_}->{got} } @$required;
506 if ( @set == 0 ) {
507 my $err = join(', ', map { "--$self->{opts}->{$_}->{long}" }
508 @{$required}[ 0 .. scalar(@$required) - 2] )
509 .' or --'.$self->{opts}->{$required->[-1]}->{long};
510 $self->save_error("Specify at least one of $err");
511 }
512 }
513
514 $self->_check_opts( keys %{$self->{opts}} );
515 $self->{got_opts} = 1;
516 return;
517}
518
519sub _check_opts {
520 my ( $self, @long ) = @_;
521 my $long_last = scalar @long;
522 while ( @long ) {
523 foreach my $i ( 0..$#long ) {
524 my $long = $long[$i];
525 next unless $long;
526 my $opt = $self->{opts}->{$long};
527 if ( $opt->{got} ) {
528 if ( exists $self->{disables}->{$long} ) {
529 my @disable_opts = @{$self->{disables}->{$long}};
530 map { $self->{opts}->{$_}->{value} = undef; } @disable_opts;
531 PTDEBUG && _d('Unset options', @disable_opts,
532 'because', $long,'disables them');
533 }
534
535 if ( exists $self->{allowed_groups}->{$long} ) {
536
537 my @restricted_groups = grep {
538 !exists $self->{allowed_groups}->{$long}->{$_}
539 } keys %{$self->{groups}};
540
541 my @restricted_opts;
542 foreach my $restricted_group ( @restricted_groups ) {
543 RESTRICTED_OPT:
544 foreach my $restricted_opt (
545 keys %{$self->{groups}->{$restricted_group}} )
546 {
547 next RESTRICTED_OPT if $restricted_opt eq $long;
548 push @restricted_opts, $restricted_opt
549 if $self->{opts}->{$restricted_opt}->{got};
550 }
551 }
552
553 if ( @restricted_opts ) {
554 my $err;
555 if ( @restricted_opts == 1 ) {
556 $err = "--$restricted_opts[0]";
557 }
558 else {
559 $err = join(', ',
560 map { "--$self->{opts}->{$_}->{long}" }
561 grep { $_ }
562 @restricted_opts[0..scalar(@restricted_opts) - 2]
563 )
564 . ' or --'.$self->{opts}->{$restricted_opts[-1]}->{long};
565 }
566 $self->save_error("--$long is not allowed with $err");
567 }
568 }
569
570 }
571 elsif ( $opt->{is_required} ) {
572 $self->save_error("Required option --$long must be specified");
573 }
574
575 $self->_validate_type($opt);
576 if ( $opt->{parsed} ) {
577 delete $long[$i];
578 }
579 else {
580 PTDEBUG && _d('Temporarily failed to parse', $long);
581 }
582 }
583
584 die "Failed to parse options, possibly due to circular dependencies"
585 if @long == $long_last;
586 $long_last = @long;
587 }
588
589 return;
590}
591
592sub _validate_type {
593 my ( $self, $opt ) = @_;
594 return unless $opt;
595
596 if ( !$opt->{type} ) {
597 $opt->{parsed} = 1;
598 return;
599 }
600
601 my $val = $opt->{value};
602
603 if ( $val && $opt->{type} eq 'm' ) { # type time
604 PTDEBUG && _d('Parsing option', $opt->{long}, 'as a time value');
605 my ( $prefix, $num, $suffix ) = $val =~ m/([+-]?)(\d+)([a-z])?$/;
606 if ( !$suffix ) {
607 my ( $s ) = $opt->{desc} =~ m/\(suffix (.)\)/;
608 $suffix = $s || 's';
609 PTDEBUG && _d('No suffix given; using', $suffix, 'for',
610 $opt->{long}, '(value:', $val, ')');
611 }
612 if ( $suffix =~ m/[smhd]/ ) {
613 $val = $suffix eq 's' ? $num # Seconds
614 : $suffix eq 'm' ? $num * 60 # Minutes
615 : $suffix eq 'h' ? $num * 3600 # Hours
616 : $num * 86400; # Days
617 $opt->{value} = ($prefix || '') . $val;
618 PTDEBUG && _d('Setting option', $opt->{long}, 'to', $val);
619 }
620 else {
621 $self->save_error("Invalid time suffix for --$opt->{long}");
622 }
623 }
624 elsif ( $val && $opt->{type} eq 'd' ) { # type DSN
625 PTDEBUG && _d('Parsing option', $opt->{long}, 'as a DSN');
626 my $prev = {};
627 my $from_key = $self->{defaults_to}->{ $opt->{long} };
628 if ( $from_key ) {
629 PTDEBUG && _d($opt->{long}, 'DSN copies from', $from_key, 'DSN');
630 if ( $self->{opts}->{$from_key}->{parsed} ) {
631 $prev = $self->{opts}->{$from_key}->{value};
632 }
633 else {
634 PTDEBUG && _d('Cannot parse', $opt->{long}, 'until',
635 $from_key, 'parsed');
636 return;
637 }
638 }
639 my $defaults = $self->{DSNParser}->parse_options($self);
640 $opt->{value} = $self->{DSNParser}->parse($val, $prev, $defaults);
641 }
642 elsif ( $val && $opt->{type} eq 'z' ) { # type size
643 PTDEBUG && _d('Parsing option', $opt->{long}, 'as a size value');
644 $self->_parse_size($opt, $val);
645 }
646 elsif ( $opt->{type} eq 'H' || (defined $val && $opt->{type} eq 'h') ) {
647 $opt->{value} = { map { $_ => 1 } split(/(?<!\\),\s*/, ($val || '')) };
648 }
649 elsif ( $opt->{type} eq 'A' || (defined $val && $opt->{type} eq 'a') ) {
650 $opt->{value} = [ split(/(?<!\\),\s*/, ($val || '')) ];
651 }
652 else {
653 PTDEBUG && _d('Nothing to validate for option',
654 $opt->{long}, 'type', $opt->{type}, 'value', $val);
655 }
656
657 $opt->{parsed} = 1;
658 return;
659}
660
661sub get {
662 my ( $self, $opt ) = @_;
663 my $long = (length $opt == 1 ? $self->{short_opts}->{$opt} : $opt);
664 die "Option $opt does not exist"
665 unless $long && exists $self->{opts}->{$long};
666 return $self->{opts}->{$long}->{value};
667}
668
669sub got {
670 my ( $self, $opt ) = @_;
671 my $long = (length $opt == 1 ? $self->{short_opts}->{$opt} : $opt);
672 die "Option $opt does not exist"
673 unless $long && exists $self->{opts}->{$long};
674 return $self->{opts}->{$long}->{got};
675}
676
677sub has {
678 my ( $self, $opt ) = @_;
679 my $long = (length $opt == 1 ? $self->{short_opts}->{$opt} : $opt);
680 return defined $long ? exists $self->{opts}->{$long} : 0;
681}
682
683sub set {
684 my ( $self, $opt, $val ) = @_;
685 my $long = (length $opt == 1 ? $self->{short_opts}->{$opt} : $opt);
686 die "Option $opt does not exist"
687 unless $long && exists $self->{opts}->{$long};
688 $self->{opts}->{$long}->{value} = $val;
689 return;
690}
691
692sub save_error {
693 my ( $self, $error ) = @_;
694 push @{$self->{errors}}, $error;
695 return;
696}
697
698sub errors {
699 my ( $self ) = @_;
700 return $self->{errors};
701}
702
703sub usage {
704 my ( $self ) = @_;
705 warn "No usage string is set" unless $self->{usage}; # XXX
706 return "Usage: " . ($self->{usage} || '') . "\n";
707}
708
709sub descr {
710 my ( $self ) = @_;
711 warn "No description string is set" unless $self->{description}; # XXX
712 my $descr = ($self->{description} || $self->{program_name} || '')
713 . " For more details, please use the --help option, "
714 . "or try 'perldoc $PROGRAM_NAME' "
715 . "for complete documentation.";
716 $descr = join("\n", $descr =~ m/(.{0,80})(?:\s+|$)/g)
717 unless $ENV{DONT_BREAK_LINES};
718 $descr =~ s/ +$//mg;
719 return $descr;
720}
721
722sub usage_or_errors {
723 my ( $self, $file, $return ) = @_;
724 $file ||= $self->{file} || __FILE__;
725
726 if ( !$self->{description} || !$self->{usage} ) {
727 PTDEBUG && _d("Getting description and usage from SYNOPSIS in", $file);
728 my %synop = $self->_parse_synopsis($file);
729 $self->{description} ||= $synop{description};
730 $self->{usage} ||= $synop{usage};
731 PTDEBUG && _d("Description:", $self->{description},
732 "\nUsage:", $self->{usage});
733 }
734
735 if ( $self->{opts}->{help}->{got} ) {
736 print $self->print_usage() or die "Cannot print usage: $OS_ERROR";
737 exit 0 unless $return;
738 }
739 elsif ( scalar @{$self->{errors}} ) {
740 print $self->print_errors() or die "Cannot print errors: $OS_ERROR";
741 exit 0 unless $return;
742 }
743
744 return;
745}
746
747sub print_errors {
748 my ( $self ) = @_;
749 my $usage = $self->usage() . "\n";
750 if ( (my @errors = @{$self->{errors}}) ) {
751 $usage .= join("\n * ", 'Errors in command-line arguments:', @errors)
752 . "\n";
753 }
754 return $usage . "\n" . $self->descr();
755}
756
757sub print_usage {
758 my ( $self ) = @_;
759 die "Run get_opts() before print_usage()" unless $self->{got_opts};
760 my @opts = values %{$self->{opts}};
761
762 my $maxl = max(
763 map {
764 length($_->{long}) # option long name
765 + ($_->{is_negatable} ? 4 : 0) # "[no]" if opt is negatable
766 + ($_->{type} ? 2 : 0) # "=x" where x is the opt type
767 }
768 @opts);
769
770 my $maxs = max(0,
771 map {
772 length($_)
773 + ($self->{opts}->{$_}->{is_negatable} ? 4 : 0)
774 + ($self->{opts}->{$_}->{type} ? 2 : 0)
775 }
776 values %{$self->{short_opts}});
777
778 my $lcol = max($maxl, ($maxs + 3));
779 my $rcol = 80 - $lcol - 6;
780 my $rpad = ' ' x ( 80 - $rcol );
781
782 $maxs = max($lcol - 3, $maxs);
783
784 my $usage = $self->descr() . "\n" . $self->usage();
785
786 my @groups = reverse sort grep { $_ ne 'default'; } keys %{$self->{groups}};
787 push @groups, 'default';
788
789 foreach my $group ( reverse @groups ) {
790 $usage .= "\n".($group eq 'default' ? 'Options' : $group).":\n\n";
791 foreach my $opt (
792 sort { $a->{long} cmp $b->{long} }
793 grep { $_->{group} eq $group }
794 @opts )
795 {
796 my $long = $opt->{is_negatable} ? "[no]$opt->{long}" : $opt->{long};
797 my $short = $opt->{short};
798 my $desc = $opt->{desc};
799
800 $long .= $opt->{type} ? "=$opt->{type}" : "";
801
802 if ( $opt->{type} && $opt->{type} eq 'm' ) {
803 my ($s) = $desc =~ m/\(suffix (.)\)/;
804 $s ||= 's';
805 $desc =~ s/\s+\(suffix .\)//;
806 $desc .= ". Optional suffix s=seconds, m=minutes, h=hours, "
807 . "d=days; if no suffix, $s is used.";
808 }
809 $desc = join("\n$rpad", grep { $_ } $desc =~ m/(.{0,$rcol})(?:\s+|$)/g);
810 $desc =~ s/ +$//mg;
811 if ( $short ) {
812 $usage .= sprintf(" --%-${maxs}s -%s %s\n", $long, $short, $desc);
813 }
814 else {
815 $usage .= sprintf(" --%-${lcol}s %s\n", $long, $desc);
816 }
817 }
818 }
819
820 $usage .= "\nOption types: s=string, i=integer, f=float, h/H/a/A=comma-separated list, d=DSN, z=size, m=time\n";
821
822 if ( (my @rules = @{$self->{rules}}) ) {
823 $usage .= "\nRules:\n\n";
824 $usage .= join("\n", map { " $_" } @rules) . "\n";
825 }
826 if ( $self->{DSNParser} ) {
827 $usage .= "\n" . $self->{DSNParser}->usage();
828 }
829 $usage .= "\nOptions and values after processing arguments:\n\n";
830 foreach my $opt ( sort { $a->{long} cmp $b->{long} } @opts ) {
831 my $val = $opt->{value};
832 my $type = $opt->{type} || '';
833 my $bool = $opt->{spec} =~ m/^[\w-]+(?:\|[\w-])?!?$/;
834 $val = $bool ? ( $val ? 'TRUE' : 'FALSE' )
835 : !defined $val ? '(No value)'
836 : $type eq 'd' ? $self->{DSNParser}->as_string($val)
837 : $type =~ m/H|h/ ? join(',', sort keys %$val)
838 : $type =~ m/A|a/ ? join(',', @$val)
839 : $val;
840 $usage .= sprintf(" --%-${lcol}s %s\n", $opt->{long}, $val);
841 }
842 return $usage;
843}
844
845sub prompt_noecho {
846 shift @_ if ref $_[0] eq __PACKAGE__;
847 my ( $prompt ) = @_;
848 local $OUTPUT_AUTOFLUSH = 1;
849 print $prompt
850 or die "Cannot print: $OS_ERROR";
851 my $response;
852 eval {
853 require Term::ReadKey;
854 Term::ReadKey::ReadMode('noecho');
855 chomp($response = <STDIN>);
856 Term::ReadKey::ReadMode('normal');
857 print "\n"
858 or die "Cannot print: $OS_ERROR";
859 };
860 if ( $EVAL_ERROR ) {
861 die "Cannot read response; is Term::ReadKey installed? $EVAL_ERROR";
862 }
863 return $response;
864}
865
866sub _read_config_file {
867 my ( $self, $filename ) = @_;
868 open my $fh, "<", $filename or die "Cannot open $filename: $OS_ERROR\n";
869 my @args;
870 my $prefix = '--';
871 my $parse = 1;
872
873 LINE:
874 while ( my $line = <$fh> ) {
875 chomp $line;
876 next LINE if $line =~ m/^\s*(?:\#|\;|$)/;
877 $line =~ s/\s+#.*$//g;
878 $line =~ s/^\s+|\s+$//g;
879 if ( $line eq '--' ) {
880 $prefix = '';
881 $parse = 0;
882 next LINE;
883 }
884 if ( $parse
885 && (my($opt, $arg) = $line =~ m/^\s*([^=\s]+?)(?:\s*=\s*(.*?)\s*)?$/)
886 ) {
887 push @args, grep { defined $_ } ("$prefix$opt", $arg);
888 }
889 elsif ( $line =~ m/./ ) {
890 push @args, $line;
891 }
892 else {
893 die "Syntax error in file $filename at line $INPUT_LINE_NUMBER";
894 }
895 }
896 close $fh;
897 return @args;
898}
899
900sub read_para_after {
901 my ( $self, $file, $regex ) = @_;
902 open my $fh, "<", $file or die "Can't open $file: $OS_ERROR";
903 local $INPUT_RECORD_SEPARATOR = '';
904 my $para;
905 while ( $para = <$fh> ) {
906 next unless $para =~ m/^=pod$/m;
907 last;
908 }
909 while ( $para = <$fh> ) {
910 next unless $para =~ m/$regex/;
911 last;
912 }
913 $para = <$fh>;
914 chomp($para);
915 close $fh or die "Can't close $file: $OS_ERROR";
916 return $para;
917}
918
919sub clone {
920 my ( $self ) = @_;
921
922 my %clone = map {
923 my $hashref = $self->{$_};
924 my $val_copy = {};
925 foreach my $key ( keys %$hashref ) {
926 my $ref = ref $hashref->{$key};
927 $val_copy->{$key} = !$ref ? $hashref->{$key}
928 : $ref eq 'HASH' ? { %{$hashref->{$key}} }
929 : $ref eq 'ARRAY' ? [ @{$hashref->{$key}} ]
930 : $hashref->{$key};
931 }
932 $_ => $val_copy;
933 } qw(opts short_opts defaults);
934
935 foreach my $scalar ( qw(got_opts) ) {
936 $clone{$scalar} = $self->{$scalar};
937 }
938
939 return bless \%clone;
940}
941
942sub _parse_size {
943 my ( $self, $opt, $val ) = @_;
944
945 if ( lc($val || '') eq 'null' ) {
946 PTDEBUG && _d('NULL size for', $opt->{long});
947 $opt->{value} = 'null';
948 return;
949 }
950
951 my %factor_for = (k => 1_024, M => 1_048_576, G => 1_073_741_824);
952 my ($pre, $num, $factor) = $val =~ m/^([+-])?(\d+)([kMG])?$/;
953 if ( defined $num ) {
954 if ( $factor ) {
955 $num *= $factor_for{$factor};
956 PTDEBUG && _d('Setting option', $opt->{y},
957 'to num', $num, '* factor', $factor);
958 }
959 $opt->{value} = ($pre || '') . $num;
960 }
961 else {
962 $self->save_error("Invalid size for --$opt->{long}: $val");
963 }
964 return;
965}
966
967sub _parse_attribs {
968 my ( $self, $option, $attribs ) = @_;
969 my $types = $self->{types};
970 return $option
971 . ($attribs->{'short form'} ? '|' . $attribs->{'short form'} : '' )
972 . ($attribs->{'negatable'} ? '!' : '' )
973 . ($attribs->{'cumulative'} ? '+' : '' )
974 . ($attribs->{'type'} ? '=' . $types->{$attribs->{type}} : '' );
975}
976
977sub _parse_synopsis {
978 my ( $self, $file ) = @_;
979 $file ||= $self->{file} || __FILE__;
980 PTDEBUG && _d("Parsing SYNOPSIS in", $file);
981
982 local $INPUT_RECORD_SEPARATOR = ''; # read paragraphs
983 open my $fh, "<", $file or die "Cannot open $file: $OS_ERROR";
984 my $para;
985 1 while defined($para = <$fh>) && $para !~ m/^=head1 SYNOPSIS/;
986 die "$file does not contain a SYNOPSIS section" unless $para;
987 my @synop;
988 for ( 1..2 ) { # 1 for the usage, 2 for the description
989 my $para = <$fh>;
990 push @synop, $para;
991 }
992 close $fh;
993 PTDEBUG && _d("Raw SYNOPSIS text:", @synop);
994 my ($usage, $desc) = @synop;
995 die "The SYNOPSIS section in $file is not formatted properly"
996 unless $usage && $desc;
997
998 $usage =~ s/^\s*Usage:\s+(.+)/$1/;
999 chomp $usage;
1000
1001 $desc =~ s/\n/ /g;
1002 $desc =~ s/\s{2,}/ /g;
1003 $desc =~ s/\. ([A-Z][a-z])/. $1/g;
1004 $desc =~ s/\s+$//;
1005
1006 return (
1007 description => $desc,
1008 usage => $usage,
1009 );
1010};
1011
1012sub _d {
1013 my ($package, undef, $line) = caller 0;
1014 @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; }
1015 map { defined $_ ? $_ : 'undef' }
1016 @_;
1017 print STDERR "# $package:$line $PID ", join(' ', @_), "\n";
1018}
1019
1020if ( PTDEBUG ) {
1021 print '# ', $^X, ' ', $], "\n";
1022 if ( my $uname = `uname -a` ) {
1023 $uname =~ s/\s+/ /g;
1024 print "# $uname\n";
1025 }
1026 print '# Arguments: ',
1027 join(' ', map { my $a = "_[$_]_"; $a =~ s/\n/\n# /g; $a; } @ARGV), "\n";
1028}
1029
10301;
1031}
1032# ###########################################################################
1033# End OptionParser package
1034# ###########################################################################
1035
1036# ###########################################################################
1037# QueryParser package
1038# This package is a copy without comments from the original. The original
1039# with comments and its test file can be found in the Bazaar repository at,
1040# lib/QueryParser.pm
1041# t/lib/QueryParser.t
1042# See https://launchpad.net/percona-toolkit for more information.
1043# ###########################################################################
1044{
1045package QueryParser;
1046
1047use strict;
1048use warnings FATAL => 'all';
1049use English qw(-no_match_vars);
1050use constant PTDEBUG => $ENV{PTDEBUG} || 0;
1051
1052our $tbl_ident = qr/(?:`[^`]+`|\w+)(?:\.(?:`[^`]+`|\w+))?/;
1053our $tbl_regex = qr{
1054 \b(?:FROM|JOIN|(?<!KEY\s)UPDATE|INTO) # Words that precede table names
1055 \b\s*
1056 \(? # Optional paren around tables
1057 ($tbl_ident
1058 (?: (?:\s+ (?:AS\s+)? \w+)?, \s*$tbl_ident )*
1059 )
1060 }xio;
1061our $has_derived = qr{
1062 \b(?:FROM|JOIN|,)
1063 \s*\(\s*SELECT
1064 }xi;
1065
1066our $data_def_stmts = qr/(?:CREATE|ALTER|TRUNCATE|DROP|RENAME)/i;
1067
1068our $data_manip_stmts = qr/(?:INSERT|UPDATE|DELETE|REPLACE)/i;
1069
1070sub new {
1071 my ( $class ) = @_;
1072 bless {}, $class;
1073}
1074
1075sub get_tables {
1076 my ( $self, $query ) = @_;
1077 return unless $query;
1078 PTDEBUG && _d('Getting tables for', $query);
1079
1080 my ( $ddl_stmt ) = $query =~ m/^\s*($data_def_stmts)\b/i;
1081 if ( $ddl_stmt ) {
1082 PTDEBUG && _d('Special table type:', $ddl_stmt);
1083 $query =~ s/IF\s+(?:NOT\s+)?EXISTS//i;
1084 if ( $query =~ m/$ddl_stmt DATABASE\b/i ) {
1085 PTDEBUG && _d('Query alters a database, not a table');
1086 return ();
1087 }
1088 if ( $ddl_stmt =~ m/CREATE/i && $query =~ m/$ddl_stmt\b.+?\bSELECT\b/i ) {
1089 my ($select) = $query =~ m/\b(SELECT\b.+)/is;
1090 PTDEBUG && _d('CREATE TABLE ... SELECT:', $select);
1091 return $self->get_tables($select);
1092 }
1093 my ($tbl) = $query =~ m/TABLE\s+($tbl_ident)(\s+.*)?/i;
1094 PTDEBUG && _d('Matches table:', $tbl);
1095 return ($tbl);
1096 }
1097
1098 $query =~ s/ (?:LOW_PRIORITY|IGNORE|STRAIGHT_JOIN)//ig;
1099
1100 if ( $query =~ /^\s*LOCK TABLES/i ) {
1101 PTDEBUG && _d('Special table type: LOCK TABLES');
1102 $query =~ s/^(\s*LOCK TABLES\s+)//;
1103 $query =~ s/\s+(?:READ|WRITE|LOCAL)+\s*//g;
1104 PTDEBUG && _d('Locked tables:', $query);
1105 $query = "FROM $query";
1106 }
1107
1108 $query =~ s/\\["']//g; # quoted strings
1109 $query =~ s/".*?"/?/sg; # quoted strings
1110 $query =~ s/'.*?'/?/sg; # quoted strings
1111
1112 my @tables;
1113 foreach my $tbls ( $query =~ m/$tbl_regex/gio ) {
1114 PTDEBUG && _d('Match tables:', $tbls);
1115
1116 next if $tbls =~ m/\ASELECT\b/i;
1117
1118 foreach my $tbl ( split(',', $tbls) ) {
1119 $tbl =~ s/\s*($tbl_ident)(\s+.*)?/$1/gio;
1120
1121 if ( $tbl !~ m/[a-zA-Z]/ ) {
1122 PTDEBUG && _d('Skipping suspicious table name:', $tbl);
1123 next;
1124 }
1125
1126 push @tables, $tbl;
1127 }
1128 }
1129 return @tables;
1130}
1131
1132sub has_derived_table {
1133 my ( $self, $query ) = @_;
1134 my $match = $query =~ m/$has_derived/;
1135 PTDEBUG && _d($query, 'has ' . ($match ? 'a' : 'no') . ' derived table');
1136 return $match;
1137}
1138
1139sub get_aliases {
1140 my ( $self, $query, $list ) = @_;
1141
1142 my $result = {
1143 DATABASE => {},
1144 TABLE => {},
1145 };
1146 return $result unless $query;
1147
1148 $query =~ s/ (?:LOW_PRIORITY|IGNORE|STRAIGHT_JOIN)//ig;
1149
1150 $query =~ s/ (?:INNER|OUTER|CROSS|LEFT|RIGHT|NATURAL)//ig;
1151
1152 my @tbl_refs;
1153 my ($tbl_refs, $from) = $query =~ m{
1154 (
1155 (FROM|INTO|UPDATE)\b\s* # Keyword before table refs
1156 .+? # Table refs
1157 )
1158 (?:\s+|\z) # If the query does not end with the table
1159 (?:WHERE|ORDER|LIMIT|HAVING|SET|VALUES|\z) # Keyword after table refs
1160 }ix;
1161
1162 if ( $tbl_refs ) {
1163
1164 if ( $query =~ m/^(?:INSERT|REPLACE)/i ) {
1165 $tbl_refs =~ s/\([^\)]+\)\s*//;
1166 }
1167
1168 PTDEBUG && _d('tbl refs:', $tbl_refs);
1169
1170 my $before_tbl = qr/(?:,|JOIN|\s|$from)+/i;
1171
1172 my $after_tbl = qr/(?:,|JOIN|ON|USING|\z)/i;
1173
1174 $tbl_refs =~ s/ = /=/g;
1175
1176 while (
1177 $tbl_refs =~ m{
1178 $before_tbl\b\s*
1179 ( ($tbl_ident) (?:\s+ (?:AS\s+)? (\w+))? )
1180 \s*$after_tbl
1181 }xgio )
1182 {
1183 my ( $tbl_ref, $db_tbl, $alias ) = ($1, $2, $3);
1184 PTDEBUG && _d('Match table:', $tbl_ref);
1185 push @tbl_refs, $tbl_ref;
1186 $alias = $self->trim_identifier($alias);
1187
1188 if ( $tbl_ref =~ m/^AS\s+\w+/i ) {
1189 PTDEBUG && _d('Subquery', $tbl_ref);
1190 $result->{TABLE}->{$alias} = undef;
1191 next;
1192 }
1193
1194 my ( $db, $tbl ) = $db_tbl =~ m/^(?:(.*?)\.)?(.*)/;
1195 $db = $self->trim_identifier($db);
1196 $tbl = $self->trim_identifier($tbl);
1197 $result->{TABLE}->{$alias || $tbl} = $tbl;
1198 $result->{DATABASE}->{$tbl} = $db if $db;
1199 }
1200 }
1201 else {
1202 PTDEBUG && _d("No tables ref in", $query);
1203 }
1204
1205 if ( $list ) {
1206 return \@tbl_refs;
1207 }
1208 else {
1209 return $result;
1210 }
1211}
1212
1213sub split {
1214 my ( $self, $query ) = @_;
1215 return unless $query;
1216 $query = $self->clean_query($query);
1217 PTDEBUG && _d('Splitting', $query);
1218
1219 my $verbs = qr{SELECT|INSERT|UPDATE|DELETE|REPLACE|UNION|CREATE}i;
1220
1221 my @split_statements = grep { $_ } split(m/\b($verbs\b(?!(?:\s*\()))/io, $query);
1222
1223 my @statements;
1224 if ( @split_statements == 1 ) {
1225 push @statements, $query;
1226 }
1227 else {
1228 for ( my $i = 0; $i <= $#split_statements; $i += 2 ) {
1229 push @statements, $split_statements[$i].$split_statements[$i+1];
1230
1231 if ( $statements[-2] && $statements[-2] =~ m/on duplicate key\s+$/i ) {
1232 $statements[-2] .= pop @statements;
1233 }
1234 }
1235 }
1236
1237 PTDEBUG && _d('statements:', map { $_ ? "<$_>" : 'none' } @statements);
1238 return @statements;
1239}
1240
1241sub clean_query {
1242 my ( $self, $query ) = @_;
1243 return unless $query;
1244 $query =~ s!/\*.*?\*/! !g; # Remove /* comment blocks */
1245 $query =~ s/^\s+//; # Remove leading spaces
1246 $query =~ s/\s+$//; # Remove trailing spaces
1247 $query =~ s/\s{2,}/ /g; # Remove extra spaces
1248 return $query;
1249}
1250
1251sub split_subquery {
1252 my ( $self, $query ) = @_;
1253 return unless $query;
1254 $query = $self->clean_query($query);
1255 $query =~ s/;$//;
1256
1257 my @subqueries;
1258 my $sqno = 0; # subquery number
1259 my $pos = 0;
1260 while ( $query =~ m/(\S+)(?:\s+|\Z)/g ) {
1261 $pos = pos($query);
1262 my $word = $1;
1263 PTDEBUG && _d($word, $sqno);
1264 if ( $word =~ m/^\(?SELECT\b/i ) {
1265 my $start_pos = $pos - length($word) - 1;
1266 if ( $start_pos ) {
1267 $sqno++;
1268 PTDEBUG && _d('Subquery', $sqno, 'starts at', $start_pos);
1269 $subqueries[$sqno] = {
1270 start_pos => $start_pos,
1271 end_pos => 0,
1272 len => 0,
1273 words => [$word],
1274 lp => 1, # left parentheses
1275 rp => 0, # right parentheses
1276 done => 0,
1277 };
1278 }
1279 else {
1280 PTDEBUG && _d('Main SELECT at pos 0');
1281 }
1282 }
1283 else {
1284 next unless $sqno; # next unless we're in a subquery
1285 PTDEBUG && _d('In subquery', $sqno);
1286 my $sq = $subqueries[$sqno];
1287 if ( $sq->{done} ) {
1288 PTDEBUG && _d('This subquery is done; SQL is for',
1289 ($sqno - 1 ? "subquery $sqno" : "the main SELECT"));
1290 next;
1291 }
1292 push @{$sq->{words}}, $word;
1293 my $lp = ($word =~ tr/\(//) || 0;
1294 my $rp = ($word =~ tr/\)//) || 0;
1295 PTDEBUG && _d('parentheses left', $lp, 'right', $rp);
1296 if ( ($sq->{lp} + $lp) - ($sq->{rp} + $rp) == 0 ) {
1297 my $end_pos = $pos - 1;
1298 PTDEBUG && _d('Subquery', $sqno, 'ends at', $end_pos);
1299 $sq->{end_pos} = $end_pos;
1300 $sq->{len} = $end_pos - $sq->{start_pos};
1301 }
1302 }
1303 }
1304
1305 for my $i ( 1..$#subqueries ) {
1306 my $sq = $subqueries[$i];
1307 next unless $sq;
1308 $sq->{sql} = join(' ', @{$sq->{words}});
1309 substr $query,
1310 $sq->{start_pos} + 1, # +1 for (
1311 $sq->{len} - 1, # -1 for )
1312 "__subquery_$i";
1313 }
1314
1315 return $query, map { $_->{sql} } grep { defined $_ } @subqueries;
1316}
1317
1318sub query_type {
1319 my ( $self, $query, $qr ) = @_;
1320 my ($type, undef) = $qr->distill_verbs($query);
1321 my $rw;
1322 if ( $type =~ m/^SELECT\b/ ) {
1323 $rw = 'read';
1324 }
1325 elsif ( $type =~ m/^$data_manip_stmts\b/
1326 || $type =~ m/^$data_def_stmts\b/ ) {
1327 $rw = 'write'
1328 }
1329
1330 return {
1331 type => $type,
1332 rw => $rw,
1333 }
1334}
1335
1336sub get_columns {
1337 my ( $self, $query ) = @_;
1338 my $cols = [];
1339 return $cols unless $query;
1340 my $cols_def;
1341
1342 if ( $query =~ m/^SELECT/i ) {
1343 $query =~ s/
1344 ^SELECT\s+
1345 (?:ALL
1346 |DISTINCT
1347 |DISTINCTROW
1348 |HIGH_PRIORITY
1349 |STRAIGHT_JOIN
1350 |SQL_SMALL_RESULT
1351 |SQL_BIG_RESULT
1352 |SQL_BUFFER_RESULT
1353 |SQL_CACHE
1354 |SQL_NO_CACHE
1355 |SQL_CALC_FOUND_ROWS
1356 )\s+
1357 /SELECT /xgi;
1358 ($cols_def) = $query =~ m/^SELECT\s+(.+?)\s+FROM/i;
1359 }
1360 elsif ( $query =~ m/^(?:INSERT|REPLACE)/i ) {
1361 ($cols_def) = $query =~ m/\(([^\)]+)\)\s*VALUE/i;
1362 }
1363
1364 PTDEBUG && _d('Columns:', $cols_def);
1365 if ( $cols_def ) {
1366 @$cols = split(',', $cols_def);
1367 map {
1368 my $col = $_;
1369 $col = s/^\s+//g;
1370 $col = s/\s+$//g;
1371 $col;
1372 } @$cols;
1373 }
1374
1375 return $cols;
1376}
1377
1378sub parse {
1379 my ( $self, $query ) = @_;
1380 return unless $query;
1381 my $parsed = {};
1382
1383 $query =~ s/\n/ /g;
1384 $query = $self->clean_query($query);
1385
1386 $parsed->{query} = $query,
1387 $parsed->{tables} = $self->get_aliases($query, 1);
1388 $parsed->{columns} = $self->get_columns($query);
1389
1390 my ($type) = $query =~ m/^(\w+)/;
1391 $parsed->{type} = lc $type;
1392
1393
1394 $parsed->{sub_queries} = [];
1395
1396 return $parsed;
1397}
1398
1399sub extract_tables {
1400 my ( $self, %args ) = @_;
1401 my $query = $args{query};
1402 my $default_db = $args{default_db};
1403 my $q = $self->{Quoter} || $args{Quoter};
1404 return unless $query;
1405 PTDEBUG && _d('Extracting tables');
1406 my @tables;
1407 my %seen;
1408 foreach my $db_tbl ( $self->get_tables($query) ) {
1409 next unless $db_tbl;
1410 next if $seen{$db_tbl}++; # Unique-ify for issue 337.
1411 my ( $db, $tbl ) = $q->split_unquote($db_tbl);
1412 push @tables, [ $db || $default_db, $tbl ];
1413 }
1414 return @tables;
1415}
1416
1417sub trim_identifier {
1418 my ($self, $str) = @_;
1419 return unless defined $str;
1420 $str =~ s/`//g;
1421 $str =~ s/^\s+//;
1422 $str =~ s/\s+$//;
1423 return $str;
1424}
1425
1426sub _d {
1427 my ($package, undef, $line) = caller 0;
1428 @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; }
1429 map { defined $_ ? $_ : 'undef' }
1430 @_;
1431 print STDERR "# $package:$line $PID ", join(' ', @_), "\n";
1432}
1433
14341;
1435}
1436# ###########################################################################
1437# End QueryParser package
1438# ###########################################################################
1439
1440# ###########################################################################
1441# QueryRewriter package
1442# This package is a copy without comments from the original. The original
1443# with comments and its test file can be found in the Bazaar repository at,
1444# lib/QueryRewriter.pm
1445# t/lib/QueryRewriter.t
1446# See https://launchpad.net/percona-toolkit for more information.
1447# ###########################################################################
1448{
1449package QueryRewriter;
1450
1451use strict;
1452use warnings FATAL => 'all';
1453use English qw(-no_match_vars);
1454use constant PTDEBUG => $ENV{PTDEBUG} || 0;
1455
1456our $verbs = qr{^SHOW|^FLUSH|^COMMIT|^ROLLBACK|^BEGIN|SELECT|INSERT
1457 |UPDATE|DELETE|REPLACE|^SET|UNION|^START|^LOCK}xi;
1458my $quote_re = qr/"(?:(?!(?<!\\)").)*"|'(?:(?!(?<!\\)').)*'/; # Costly!
1459my $bal;
1460$bal = qr/
1461 \(
1462 (?:
1463 (?> [^()]+ ) # Non-parens without backtracking
1464 |
1465 (??{ $bal }) # Group with matching parens
1466 )*
1467 \)
1468 /x;
1469
1470my $olc_re = qr/(?:--|#)[^'"\r\n]*(?=[\r\n]|\Z)/; # One-line comments
1471my $mlc_re = qr#/\*[^!].*?\*/#sm; # But not /*!version */
1472my $vlc_re = qr#/\*.*?[0-9+].*?\*/#sm; # For SHOW + /*!version */
1473my $vlc_rf = qr#^(SHOW).*?/\*![0-9+].*?\*/#sm; # Variation for SHOW
1474
1475
1476sub new {
1477 my ( $class, %args ) = @_;
1478 my $self = { %args };
1479 return bless $self, $class;
1480}
1481
1482sub strip_comments {
1483 my ( $self, $query ) = @_;
1484 return unless $query;
1485 $query =~ s/$olc_re//go;
1486 $query =~ s/$mlc_re//go;
1487 if ( $query =~ m/$vlc_rf/i ) { # contains show + version
1488 $query =~ s/$vlc_re//go;
1489 }
1490 return $query;
1491}
1492
1493sub shorten {
1494 my ( $self, $query, $length ) = @_;
1495 $query =~ s{
1496 \A(
1497 (?:INSERT|REPLACE)
1498 (?:\s+LOW_PRIORITY|DELAYED|HIGH_PRIORITY|IGNORE)?
1499 (?:\s\w+)*\s+\S+\s+VALUES\s*\(.*?\)
1500 )
1501 \s*,\s*\(.*?(ON\s+DUPLICATE|\Z)}
1502 {$1 /*... omitted ...*/$2}xsi;
1503
1504 return $query unless $query =~ m/IN\s*\(\s*(?!select)/i;
1505
1506 my $last_length = 0;
1507 my $query_length = length($query);
1508 while (
1509 $length > 0
1510 && $query_length > $length
1511 && $query_length < ( $last_length || $query_length + 1 )
1512 ) {
1513 $last_length = $query_length;
1514 $query =~ s{
1515 (\bIN\s*\() # The opening of an IN list
1516 ([^\)]+) # Contents of the list, assuming no item contains paren
1517 (?=\)) # Close of the list
1518 }
1519 {
1520 $1 . __shorten($2)
1521 }gexsi;
1522 }
1523
1524 return $query;
1525}
1526
1527sub __shorten {
1528 my ( $snippet ) = @_;
1529 my @vals = split(/,/, $snippet);
1530 return $snippet unless @vals > 20;
1531 my @keep = splice(@vals, 0, 20); # Remove and save the first 20 items
1532 return
1533 join(',', @keep)
1534 . "/*... omitted "
1535 . scalar(@vals)
1536 . " items ...*/";
1537}
1538
1539sub fingerprint {
1540 my ( $self, $query ) = @_;
1541
1542 $query =~ m#\ASELECT /\*!40001 SQL_NO_CACHE \*/ \* FROM `# # mysqldump query
1543 && return 'mysqldump';
1544 $query =~ m#/\*\w+\.\w+:[0-9]/[0-9]\*/# # pt-table-checksum, etc query
1545 && return 'percona-toolkit';
1546 $query =~ m/\Aadministrator command: /
1547 && return $query;
1548 $query =~ m/\A\s*(call\s+\S+)\(/i
1549 && return lc($1); # Warning! $1 used, be careful.
1550 if ( my ($beginning) = $query =~ m/\A((?:INSERT|REPLACE)(?: IGNORE)?\s+INTO.+?VALUES\s*\(.*?\))\s*,\s*\(/is ) {
1551 $query = $beginning; # Shorten multi-value INSERT statements ASAP
1552 }
1553
1554 $query =~ s/$olc_re//go;
1555 $query =~ s/$mlc_re//go;
1556 $query =~ s/\Ause \S+\Z/use ?/i # Abstract the DB in USE
1557 && return $query;
1558
1559 $query =~ s/\\["']//g; # quoted strings
1560 $query =~ s/".*?"/?/sg; # quoted strings
1561 $query =~ s/'.*?'/?/sg; # quoted strings
1562
1563 if ( $self->{match_md5_checksums} ) {
1564 $query =~ s/([._-])[a-f0-9]{32}/$1?/g;
1565 }
1566
1567 if ( !$self->{match_embedded_numbers} ) {
1568 $query =~ s/[0-9+-][0-9a-f.xb+-]*/?/g;
1569 }
1570 else {
1571 $query =~ s/\b[0-9+-][0-9a-f.xb+-]*/?/g;
1572 }
1573
1574 if ( $self->{match_md5_checksums} ) {
1575 $query =~ s/[xb+-]\?/?/g;
1576 }
1577 else {
1578 $query =~ s/[xb.+-]\?/?/g;
1579 }
1580
1581 $query =~ s/\A\s+//; # Chop off leading whitespace
1582 chomp $query; # Kill trailing whitespace
1583 $query =~ tr[ \n\t\r\f][ ]s; # Collapse whitespace
1584 $query = lc $query;
1585 $query =~ s/\bnull\b/?/g; # Get rid of NULLs
1586 $query =~ s{ # Collapse IN and VALUES lists
1587 \b(in|values?)(?:[\s,]*\([\s?,]*\))+
1588 }
1589 {$1(?+)}gx;
1590 $query =~ s{ # Collapse UNION
1591 \b(select\s.*?)(?:(\sunion(?:\sall)?)\s\1)+
1592 }
1593 {$1 /*repeat$2*/}xg;
1594 $query =~ s/\blimit \?(?:, ?\?| offset \?)?/limit ?/; # LIMIT
1595
1596 if ( $query =~ m/\bORDER BY /gi ) { # Find, anchor on ORDER BY clause
1597 1 while $query =~ s/\G(.+?)\s+ASC/$1/gi && pos $query;
1598 }
1599
1600 return $query;
1601}
1602
1603sub distill_verbs {
1604 my ( $self, $query ) = @_;
1605
1606 $query =~ m/\A\s*call\s+(\S+)\(/i && return "CALL $1";
1607 $query =~ m/\A\s*use\s+/ && return "USE";
1608 $query =~ m/\A\s*UNLOCK TABLES/i && return "UNLOCK";
1609 $query =~ m/\A\s*xa\s+(\S+)/i && return "XA_$1";
1610
1611 if ( $query =~ m/\Aadministrator command:/ ) {
1612 $query =~ s/administrator command:/ADMIN/;
1613 $query = uc $query;
1614 return $query;
1615 }
1616
1617 $query = $self->strip_comments($query);
1618
1619 if ( $query =~ m/\A\s*SHOW\s+/i ) {
1620 PTDEBUG && _d($query);
1621
1622 $query = uc $query;
1623 $query =~ s/\s+(?:GLOBAL|SESSION|FULL|STORAGE|ENGINE)\b/ /g;
1624 $query =~ s/\s+COUNT[^)]+\)//g;
1625
1626 $query =~ s/\s+(?:FOR|FROM|LIKE|WHERE|LIMIT|IN)\b.+//ms;
1627
1628 $query =~ s/\A(SHOW(?:\s+\S+){1,2}).*\Z/$1/s;
1629 $query =~ s/\s+/ /g;
1630 PTDEBUG && _d($query);
1631 return $query;
1632 }
1633
1634 eval $QueryParser::data_def_stmts;
1635 eval $QueryParser::tbl_ident;
1636 my ( $dds ) = $query =~ /^\s*($QueryParser::data_def_stmts)\b/i;
1637 if ( $dds) {
1638 my ( $obj ) = $query =~ m/$dds.+(DATABASE|TABLE)\b/i;
1639 $obj = uc $obj if $obj;
1640 PTDEBUG && _d('Data def statment:', $dds, 'obj:', $obj);
1641 my ($db_or_tbl)
1642 = $query =~ m/(?:TABLE|DATABASE)\s+($QueryParser::tbl_ident)(\s+.*)?/i;
1643 PTDEBUG && _d('Matches db or table:', $db_or_tbl);
1644 return uc($dds . ($obj ? " $obj" : '')), $db_or_tbl;
1645 }
1646
1647 my @verbs = $query =~ m/\b($verbs)\b/gio;
1648 @verbs = do {
1649 my $last = '';
1650 grep { my $pass = $_ ne $last; $last = $_; $pass } map { uc } @verbs;
1651 };
1652
1653 if ( ($verbs[0] || '') eq 'SELECT' && @verbs > 1 ) {
1654 PTDEBUG && _d("False-positive verbs after SELECT:", @verbs[1..$#verbs]);
1655 my $union = grep { $_ eq 'UNION' } @verbs;
1656 @verbs = $union ? qw(SELECT UNION) : qw(SELECT);
1657 }
1658
1659 my $verb_str = join(q{ }, @verbs);
1660 return $verb_str;
1661}
1662
1663sub __distill_tables {
1664 my ( $self, $query, $table, %args ) = @_;
1665 my $qp = $args{QueryParser} || $self->{QueryParser};
1666 die "I need a QueryParser argument" unless $qp;
1667
1668 my @tables = map {
1669 $_ =~ s/`//g;
1670 $_ =~ s/(_?)[0-9]+/$1?/g;
1671 $_;
1672 } grep { defined $_ } $qp->get_tables($query);
1673
1674 push @tables, $table if $table;
1675
1676 @tables = do {
1677 my $last = '';
1678 grep { my $pass = $_ ne $last; $last = $_; $pass } @tables;
1679 };
1680
1681 return @tables;
1682}
1683
1684sub distill {
1685 my ( $self, $query, %args ) = @_;
1686
1687 if ( $args{generic} ) {
1688 my ($cmd, $arg) = $query =~ m/^(\S+)\s+(\S+)/;
1689 return '' unless $cmd;
1690 $query = (uc $cmd) . ($arg ? " $arg" : '');
1691 }
1692 else {
1693 my ($verbs, $table) = $self->distill_verbs($query, %args);
1694
1695 if ( $verbs && $verbs =~ m/^SHOW/ ) {
1696 my %alias_for = qw(
1697 SCHEMA DATABASE
1698 KEYS INDEX
1699 INDEXES INDEX
1700 );
1701 map { $verbs =~ s/$_/$alias_for{$_}/ } keys %alias_for;
1702 $query = $verbs;
1703 }
1704 else {
1705 my @tables = $self->__distill_tables($query, $table, %args);
1706 $query = join(q{ }, $verbs, @tables);
1707 }
1708 }
1709
1710 if ( $args{trf} ) {
1711 $query = $args{trf}->($query, %args);
1712 }
1713
1714 return $query;
1715}
1716
1717sub convert_to_select {
1718 my ( $self, $query ) = @_;
1719 return unless $query;
1720
1721 return if $query =~ m/=\s*\(\s*SELECT /i;
1722
1723 $query =~ s{
1724 \A.*?
1725 update(?:\s+(?:low_priority|ignore))?\s+(.*?)
1726 \s+set\b(.*?)
1727 (?:\s*where\b(.*?))?
1728 (limit\s*[0-9]+(?:\s*,\s*[0-9]+)?)?
1729 \Z
1730 }
1731 {__update_to_select($1, $2, $3, $4)}exsi
1732 || $query =~ s{
1733 \A.*?
1734 (?:insert(?:\s+ignore)?|replace)\s+
1735 .*?\binto\b(.*?)\(([^\)]+)\)\s*
1736 values?\s*(\(.*?\))\s*
1737 (?:\blimit\b|on\s+duplicate\s+key.*)?\s*
1738 \Z
1739 }
1740 {__insert_to_select($1, $2, $3)}exsi
1741 || $query =~ s{
1742 \A.*?
1743 (?:insert(?:\s+ignore)?|replace)\s+
1744 (?:.*?\binto)\b(.*?)\s*
1745 set\s+(.*?)\s*
1746 (?:\blimit\b|on\s+duplicate\s+key.*)?\s*
1747 \Z
1748 }
1749 {__insert_to_select_with_set($1, $2)}exsi
1750 || $query =~ s{
1751 \A.*?
1752 delete\s+(.*?)
1753 \bfrom\b(.*)
1754 \Z
1755 }
1756 {__delete_to_select($1, $2)}exsi;
1757 $query =~ s/\s*on\s+duplicate\s+key\s+update.*\Z//si;
1758 $query =~ s/\A.*?(?=\bSELECT\s*\b)//ism;
1759 return $query;
1760}
1761
1762sub convert_select_list {
1763 my ( $self, $query ) = @_;
1764 $query =~ s{
1765 \A\s*select(.*?)\bfrom\b
1766 }
1767 {$1 =~ m/\*/ ? "select 1 from" : "select isnull(coalesce($1)) from"}exi;
1768 return $query;
1769}
1770
1771sub __delete_to_select {
1772 my ( $delete, $join ) = @_;
1773 if ( $join =~ m/\bjoin\b/ ) {
1774 return "select 1 from $join";
1775 }
1776 return "select * from $join";
1777}
1778
1779sub __insert_to_select {
1780 my ( $tbl, $cols, $vals ) = @_;
1781 PTDEBUG && _d('Args:', @_);
1782 my @cols = split(/,/, $cols);
1783 PTDEBUG && _d('Cols:', @cols);
1784 $vals =~ s/^\(|\)$//g; # Strip leading/trailing parens
1785 my @vals = $vals =~ m/($quote_re|[^,]*${bal}[^,]*|[^,]+)/g;
1786 PTDEBUG && _d('Vals:', @vals);
1787 if ( @cols == @vals ) {
1788 return "select * from $tbl where "
1789 . join(' and ', map { "$cols[$_]=$vals[$_]" } (0..$#cols));
1790 }
1791 else {
1792 return "select * from $tbl limit 1";
1793 }
1794}
1795
1796sub __insert_to_select_with_set {
1797 my ( $from, $set ) = @_;
1798 $set =~ s/,/ and /g;
1799 return "select * from $from where $set ";
1800}
1801
1802sub __update_to_select {
1803 my ( $from, $set, $where, $limit ) = @_;
1804 return "select $set from $from "
1805 . ( $where ? "where $where" : '' )
1806 . ( $limit ? " $limit " : '' );
1807}
1808
1809sub wrap_in_derived {
1810 my ( $self, $query ) = @_;
1811 return unless $query;
1812 return $query =~ m/\A\s*select/i
1813 ? "select 1 from ($query) as x limit 1"
1814 : $query;
1815}
1816
1817sub _d {
1818 my ($package, undef, $line) = caller 0;
1819 @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; }
1820 map { defined $_ ? $_ : 'undef' }
1821 @_;
1822 print STDERR "# $package:$line $PID ", join(' ', @_), "\n";
1823}
1824
18251;
1826}
1827# ###########################################################################
1828# End QueryRewriter package
1829# ###########################################################################
1830
1831# ###########################################################################
1832# This is a combination of modules and programs in one -- a runnable module.
1833# http://www.perl.com/pub/a/2006/07/13/lightning-articles.html?page=last
1834# Or, look it up in the Camel book on pages 642 and 643 in the 3rd edition.
1835#
1836# Check at the end of this package for the call to main() which actually runs
1837# the program.
1838# ###########################################################################
1839package pt_fingerprint;
1840
1841use English qw(-no_match_vars);
1842use Data::Dumper;
1843$Data::Dumper::Indent = 1;
1844$OUTPUT_AUTOFLUSH = 1;
1845
1846use constant MKDEBUG => $ENV{MKDEBUG} || 0;
1847
1848sub main {
1849 @ARGV = @_; # set global ARGV for this package
1850
1851 # ##########################################################################
1852 # Get configuration information.
1853 # ##########################################################################
1854 my $o = new OptionParser();
1855 $o->get_specs();
1856 $o->get_opts();
1857 $o->usage_or_errors();
1858
1859 my $qp = new QueryParser();
1860 my $qr = new QueryRewriter(
1861 QueryParser => $qp,
1862 match_md5_checksums => $o->get('match-md5-checksums'),
1863 match_embedded_numbers => $o->get('match-embedded-numbers'),
1864 );
1865
1866 if ( $o->got('query') ) {
1867 print $qr->fingerprint($o->get('query')), "\n";
1868 }
1869 else {
1870 local $INPUT_RECORD_SEPARATOR = ";\n";
1871 while ( <> ) {
1872 my $query = $_;
1873 chomp $query;
1874 $query =~ s/^#.+$//mg;
1875 $query =~ s/^\s+//;
1876 next unless $query =~ m/^\w/;
1877 print $qr->fingerprint($query), "\n";
1878 }
1879 }
1880}
1881
1882# ############################################################################
1883# Run the program.
1884# ############################################################################
1885if ( !caller ) { exit main(@ARGV); }
1886
18871; # Because this is a module as well as a script.
1888
1889# #############################################################################
1890# Documentation.
1891# #############################################################################
1892
1893=pod
1894
1895=head1 NAME
1896
1897pt-fingerprint - Convert queries into fingerprints.
1898
1899=head1 SYNOPSIS
1900
1901Usage: pt-fingerprint [OPTIONS] [FILES]
1902
1903pt-fingerprint converts queries into fingerprints. With the --query
1904option, converts the option's value into a fingerprint. With no options, treats
1905command-line arguments as FILEs and reads and converts semicolon-separated
1906queries from the FILEs. When FILE is -, it read standard input.
1907
1908Convert a single query:
1909
1910 pt-fingerprint --query "select a, b, c from users where id = 500"
1911
1912Convert a file full of queries:
1913
1914 pt-fingerprint /path/to/file.txt
1915
1916=head1 RISKS
1917
1918The following section is included to inform users about the potential risks,
1919whether known or unknown, of using this tool. The two main categories of risks
1920are those created by the nature of the tool (e.g. read-only tools vs. read-write
1921tools) and those created by bugs.
1922
1923The pt-fingerprint tool simply reads data and transforms it, so risks are
1924minimal.
1925
1926See also L<"BUGS"> for more information on filing bugs and getting help.
1927
1928=head1 DESCRIPTION
1929
1930A query fingerprint is the abstracted form of a query, which makes it possible
1931to group similar queries together. Abstracting a query removes literal values,
1932normalizes whitespace, and so on. For example, consider these two queries:
1933
1934 SELECT name, password FROM user WHERE id='12823';
1935 select name, password from user
1936 where id=5;
1937
1938Both of those queries will fingerprint to
1939
1940 select name, password from user where id=?
1941
1942Once the query's fingerprint is known, we can then talk about a query as though
1943it represents all similar queries.
1944
1945Query fingerprinting accommodates a great many special cases, which have proven
1946necessary in the real world. For example, an IN list with 5 literals is really
1947equivalent to one with 4 literals, so lists of literals are collapsed to a
1948single one. If you want to understand more about how and why all of these cases
1949are handled, please review the test cases in the Subversion repository. If you
1950find something that is not fingerprinted properly, please submit a bug report
1951with a reproducible test case. Here is a list of transformations during
1952fingerprinting, which might not be exhaustive:
1953
1954=over
1955
1956=item *
1957
1958Group all SELECT queries from mysqldump together, even if they are against
1959different tables. Ditto for all of pt-table-checksum's checksum queries.
1960
1961=item *
1962
1963Shorten multi-value INSERT statements to a single VALUES() list.
1964
1965=item *
1966
1967Strip comments.
1968
1969=item *
1970
1971Abstract the databases in USE statements, so all USE statements are grouped
1972together.
1973
1974=item *
1975
1976Replace all literals, such as quoted strings. For efficiency, the code that
1977replaces literal numbers is somewhat non-selective, and might replace some
1978things as numbers when they really are not. Hexadecimal literals are also
1979replaced. NULL is treated as a literal. Numbers embedded in identifiers are
1980also replaced, so tables named similarly will be fingerprinted to the same
1981values (e.g. users_2009 and users_2010 will fingerprint identically).
1982
1983=item *
1984
1985Collapse all whitespace into a single space.
1986
1987=item *
1988
1989Lowercase the entire query.
1990
1991=item *
1992
1993Replace all literals inside of IN() and VALUES() lists with a single
1994placeholder, regardless of cardinality.
1995
1996=item *
1997
1998Collapse multiple identical UNION queries into a single one.
1999
2000=back
2001
2002=head1 OPTIONS
2003
2004This tool accepts additional command-line arguments. Refer to the
2005L<"SYNOPSIS"> and usage information for details.
2006
2007=over
2008
2009=item --config
2010
2011type: Array
2012
2013Read this comma-separated list of config files; if specified, this must be the
2014first option on the command line.
2015
2016=item --help
2017
2018Show help and exit.
2019
2020=item --match-embedded-numbers
2021
2022Match numbers embedded in words and replace as single values. This option
2023causes the tool to be more careful about matching numbers so that words
2024with numbers, like C<catch22> are matched and replaced as a single C<?>
2025placeholder. Otherwise the default number matching pattern will replace
2026C<catch22> as C<catch?>.
2027
2028This is helpful if database or table names contain numbers.
2029
2030=item --match-md5-checksums
2031
2032Match MD5 checksums and replace as single values. This option causes
2033the tool to be more careful about matching numbers so that MD5 checksums
2034like C<fbc5e685a5d3d45aa1d0347fdb7c4d35> are matched and replaced as a
2035single C<?> placeholder. Otherwise, the default number matching pattern will
2036replace C<fbc5e685a5d3d45aa1d0347fdb7c4d35> as C<fbc?>.
2037
2038=item --query
2039
2040type: string
2041
2042The query to convert into a fingerprint.
2043
2044=item --version
2045
2046Show version and exit.
2047
2048=back
2049
2050=head1 ENVIRONMENT
2051
2052The environment variable C<PTDEBUG> enables verbose debugging output to STDERR.
2053To enable debugging and capture all output to a file, run the tool like:
2054
2055 PTDEBUG=1 pt-fingerprint ... > FILE 2>&1
2056
2057Be careful: debugging output is voluminous and can generate several megabytes
2058of output.
2059
2060=head1 SYSTEM REQUIREMENTS
2061
2062You need Perl, DBI, DBD::mysql, and some core packages that ought to be
2063installed in any reasonably new version of Perl.
2064
2065=head1 BUGS
2066
2067For a list of known bugs, see L<http://www.percona.com/bugs/pt-fingerprint>.
2068
2069Please report bugs at L<https://bugs.launchpad.net/percona-toolkit>.
2070Include the following information in your bug report:
2071
2072=over
2073
2074=item * Complete command-line used to run the tool
2075
2076=item * Tool L<"--version">
2077
2078=item * MySQL version of all servers involved
2079
2080=item * Output from the tool including STDERR
2081
2082=item * Input files (log/dump/config files, etc.)
2083
2084=back
2085
2086If possible, include debugging output by running the tool with C<PTDEBUG>;
2087see L<"ENVIRONMENT">.
2088
2089=head1 DOWNLOADING
2090
2091Visit L<http://www.percona.com/software/percona-toolkit/> to download the
2092latest release of Percona Toolkit. Or, get the latest release from the
2093command line:
2094
2095 wget percona.com/get/percona-toolkit.tar.gz
2096
2097 wget percona.com/get/percona-toolkit.rpm
2098
2099 wget percona.com/get/percona-toolkit.deb
2100
2101You can also get individual tools from the latest release:
2102
2103 wget percona.com/get/TOOL
2104
2105Replace C<TOOL> with the name of any tool.
2106
2107=head1 AUTHORS
2108
2109Baron Schwartz and Daniel Nichter
2110
2111=head1 ABOUT PERCONA TOOLKIT
2112
2113This tool is part of Percona Toolkit, a collection of advanced command-line
2114tools developed by Percona for MySQL support and consulting. Percona Toolkit
2115was forked from two projects in June, 2011: Maatkit and Aspersa. Those
2116projects were created by Baron Schwartz and developed primarily by him and
2117Daniel Nichter, both of whom are employed by Percona. Visit
2118L<http://www.percona.com/software/> for more software developed by Percona.
2119
2120=head1 COPYRIGHT, LICENSE, AND WARRANTY
2121
2122This program is copyright 2011-2012 Percona Inc.
2123Feedback and improvements are welcome.
2124
2125THIS PROGRAM IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
2126WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
2127MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
2128
2129This program is free software; you can redistribute it and/or modify it under
2130the terms of the GNU General Public License as published by the Free Software
2131Foundation, version 2; OR the Perl Artistic License. On UNIX and similar
2132systems, you can issue `man perlgpl' or `man perlartistic' to read these
2133licenses.
2134
2135You should have received a copy of the GNU General Public License along with
2136this program; if not, write to the Free Software Foundation, Inc., 59 Temple
2137Place, Suite 330, Boston, MA 02111-1307 USA.
2138
2139=head1 VERSION
2140
2141pt-fingerprint 2.0.0
2142
2143=cut
02144
=== modified file 'bin/pt-index-usage'
--- bin/pt-index-usage 2012-03-07 23:41:54 +0000
+++ bin/pt-index-usage 2012-03-31 16:07:24 +0000
@@ -2669,19 +2669,58 @@
2669 return bless $self, $class;2669 return bless $self, $class;
2670}2670}
26712671
2672sub get_create_table {
2673 my ( $self, $dbh, $db, $tbl ) = @_;
2674 die "I need a dbh parameter" unless $dbh;
2675 die "I need a db parameter" unless $db;
2676 die "I need a tbl parameter" unless $tbl;
2677 my $q = $self->{Quoter};
2678
2679 my $new_sql_mode
2680 = '/*!40101 SET @OLD_SQL_MODE := @@SQL_MODE, '
2681 . q{@@SQL_MODE := REPLACE(REPLACE(@@SQL_MODE, 'ANSI_QUOTES', ''), ',,', ','), }
2682 . '@OLD_QUOTE := @@SQL_QUOTE_SHOW_CREATE, '
2683 . '@@SQL_QUOTE_SHOW_CREATE := 1 */';
2684
2685 my $old_sql_mode = '/*!40101 SET @@SQL_MODE := @OLD_SQL_MODE, '
2686 . '@@SQL_QUOTE_SHOW_CREATE := @OLD_QUOTE */';
2687
2688 PTDEBUG && _d($new_sql_mode);
2689 eval { $dbh->do($new_sql_mode); };
2690 PTDEBUG && $EVAL_ERROR && _d($EVAL_ERROR);
2691
2692 my $use_sql = 'USE ' . $q->quote($db);
2693 PTDEBUG && _d($dbh, $use_sql);
2694 $dbh->do($use_sql);
2695
2696 my $show_sql = "SHOW CREATE TABLE " . $q->quote($db, $tbl);
2697 PTDEBUG && _d($show_sql);
2698 my $href;
2699 eval { $href = $dbh->selectrow_hashref($show_sql); };
2700 if ( $EVAL_ERROR ) {
2701 PTDEBUG && _d($EVAL_ERROR);
2702
2703 PTDEBUG && _d($old_sql_mode);
2704 $dbh->do($old_sql_mode);
2705
2706 return;
2707 }
2708
2709 PTDEBUG && _d($old_sql_mode);
2710 $dbh->do($old_sql_mode);
2711
2712 my ($key) = grep { m/create (?:table|view)/i } keys %$href;
2713 if ( !$key ) {
2714 die "Error: no 'Create Table' or 'Create View' in result set from "
2715 . "$show_sql: " . Dumper($href);
2716 }
2717
2718 return $href->{$key};
2719}
2720
2672sub parse {2721sub parse {
2673 my ( $self, $ddl, $opts ) = @_;2722 my ( $self, $ddl, $opts ) = @_;
2674 return unless $ddl;2723 return unless $ddl;
2675 if ( ref $ddl eq 'ARRAY' ) {
2676 if ( lc $ddl->[0] eq 'table' ) {
2677 $ddl = $ddl->[1];
2678 }
2679 else {
2680 return {
2681 engine => 'VIEW',
2682 };
2683 }
2684 }
26852724
2686 if ( $ddl !~ m/CREATE (?:TEMPORARY )?TABLE `/ ) {2725 if ( $ddl !~ m/CREATE (?:TEMPORARY )?TABLE `/ ) {
2687 die "Cannot parse table definition; is ANSI quoting "2726 die "Cannot parse table definition; is ANSI quoting "
@@ -2988,41 +3027,31 @@
2988 return $ddl;3027 return $ddl;
2989}3028}
29903029
2991sub remove_secondary_indexes {3030sub get_table_status {
2992 my ( $self, $ddl ) = @_;3031 my ( $self, $dbh, $db, $like ) = @_;
2993 my $sec_indexes_ddl;3032 my $q = $self->{Quoter};
2994 my $tbl_struct = $self->parse($ddl);3033 my $sql = "SHOW TABLE STATUS FROM " . $q->quote($db);
29953034 my @params;
2996 if ( ($tbl_struct->{engine} || '') =~ m/InnoDB/i ) {3035 if ( $like ) {
2997 my $clustered_key = $tbl_struct->{clustered_key};3036 $sql .= ' LIKE ?';
2998 $clustered_key ||= '';3037 push @params, $like;
29993038 }
3000 my @sec_indexes = map {3039 PTDEBUG && _d($sql, @params);
3001 my $key_def = $_->{ddl};3040 my $sth = $dbh->prepare($sql);
3002 $key_def =~ s/([\(\)])/\\$1/g;3041 eval { $sth->execute(@params); };
3003 $ddl =~ s/\s+$key_def//i;3042 if ($EVAL_ERROR) {
30043043 PTDEBUG && _d($EVAL_ERROR);
3005 my $key_ddl = "ADD $_->{ddl}";3044 return;
3006 $key_ddl .= ',' unless $key_ddl =~ m/,$/;3045 }
3007 $key_ddl;3046 my @tables = @{$sth->fetchall_arrayref({})};
3008 }3047 @tables = map {
3009 grep { $_->{name} ne $clustered_key }3048 my %tbl; # Make a copy with lowercased keys
3010 values %{$tbl_struct->{keys}};3049 @tbl{ map { lc $_ } keys %$_ } = values %$_;
3011 PTDEBUG && _d('Secondary indexes:', Dumper(\@sec_indexes));3050 $tbl{engine} ||= $tbl{type} || $tbl{comment};
30123051 delete $tbl{type};
3013 if ( @sec_indexes ) {3052 \%tbl;
3014 $sec_indexes_ddl = join(' ', @sec_indexes);3053 } @tables;
3015 $sec_indexes_ddl =~ s/,$//;3054 return @tables;
3016 }
3017
3018 $ddl =~ s/,(\n\) )/$1/s;
3019 }
3020 else {
3021 PTDEBUG && _d('Not removing secondary indexes from',
3022 $tbl_struct->{engine}, 'table');
3023 }
3024
3025 return $ddl, $sec_indexes_ddl, $tbl_struct;
3026}3055}
30273056
3028sub _d {3057sub _d {
@@ -3912,7 +3941,7 @@
39123941
3913sub new {3942sub new {
3914 my ( $class, %args ) = @_;3943 my ( $class, %args ) = @_;
3915 my @required_args = qw(OptionParser Quoter);3944 my @required_args = qw(OptionParser TableParser Quoter);
3916 foreach my $arg ( @required_args ) {3945 foreach my $arg ( @required_args ) {
3917 die "I need a $arg argument" unless $args{$arg};3946 die "I need a $arg argument" unless $args{$arg};
3918 }3947 }
@@ -3921,8 +3950,19 @@
3921 die "I need either a dbh or file_itr argument"3950 die "I need either a dbh or file_itr argument"
3922 if (!$dbh && !$file_itr) || ($dbh && $file_itr);3951 if (!$dbh && !$file_itr) || ($dbh && $file_itr);
39233952
3953 my %resume;
3954 if ( my $table = $args{resume} ) {
3955 PTDEBUG && _d('Will resume from or after', $table);
3956 my ($db, $tbl) = $args{Quoter}->split_unquote($table);
3957 die "Resume table must be database-qualified: $table"
3958 unless $db && $tbl;
3959 $resume{db} = $db;
3960 $resume{tbl} = $tbl;
3961 }
3962
3924 my $self = {3963 my $self = {
3925 %args,3964 %args,
3965 resume => \%resume,
3926 filters => _make_filters(%args),3966 filters => _make_filters(%args),
3927 };3967 };
39283968
@@ -3983,9 +4023,19 @@
3983 return \%filters;4023 return \%filters;
3984}4024}
39854025
3986sub next_schema_object {4026sub next {
3987 my ( $self ) = @_;4027 my ( $self ) = @_;
39884028
4029 if ( !$self->{initialized} ) {
4030 $self->{initialized} = 1;
4031 if ( $self->{resume}->{tbl}
4032 && !$self->table_is_allowed(@{$self->{resume}}{qw(db tbl)}) ) {
4033 PTDEBUG && _d('Will resume after',
4034 join('.', @{$self->{resume}}{qw(db tbl)}));
4035 $self->{resume}->{after} = 1;
4036 }
4037 }
4038
3989 my $schema_obj;4039 my $schema_obj;
3990 if ( $self->{file_itr} ) {4040 if ( $self->{file_itr} ) {
3991 $schema_obj= $self->_iterate_files();4041 $schema_obj= $self->_iterate_files();
@@ -3995,24 +4045,18 @@
3995 }4045 }
39964046
3997 if ( $schema_obj ) {4047 if ( $schema_obj ) {
3998 if ( $schema_obj->{ddl} && $self->{TableParser} ) {
3999 $schema_obj->{tbl_struct}
4000 = $self->{TableParser}->parse($schema_obj->{ddl});
4001 }
4002
4003 delete $schema_obj->{ddl} unless $self->{keep_ddl};
4004
4005 if ( my $schema = $self->{Schema} ) {4048 if ( my $schema = $self->{Schema} ) {
4006 $schema->add_schema_object($schema_obj);4049 $schema->add_schema_object($schema_obj);
4007 }4050 }
4051 PTDEBUG && _d('Next schema object:',
4052 $schema_obj->{db}, $schema_obj->{tbl});
4008 }4053 }
40094054
4010 PTDEBUG && _d('Next schema object:', $schema_obj->{db}, $schema_obj->{tbl});
4011 return $schema_obj;4055 return $schema_obj;
4012}4056}
40134057
4014sub _iterate_files {4058sub _iterate_files {
4015 my ( $self ) = @_;4059 my ( $self ) = @_;
40164060
4017 if ( !$self->{fh} ) {4061 if ( !$self->{fh} ) {
4018 my ($fh, $file) = $self->{file_itr}->();4062 my ($fh, $file) = $self->{file_itr}->();
@@ -4033,7 +4077,8 @@
4033 my $db = $1; # XXX4077 my $db = $1; # XXX
4034 $db =~ s/^`//; # strip leading `4078 $db =~ s/^`//; # strip leading `
4035 $db =~ s/`$//; # and trailing `4079 $db =~ s/`$//; # and trailing `
4036 if ( $self->database_is_allowed($db) ) {4080 if ( $self->database_is_allowed($db)
4081 && $self->_resume_from_database($db) ) {
4037 $self->{db} = $db;4082 $self->{db} = $db;
4038 }4083 }
4039 }4084 }
@@ -4046,21 +4091,22 @@
4046 my ($tbl) = $chunk =~ m/$tbl_name/;4091 my ($tbl) = $chunk =~ m/$tbl_name/;
4047 $tbl =~ s/^\s*`//;4092 $tbl =~ s/^\s*`//;
4048 $tbl =~ s/`\s*$//;4093 $tbl =~ s/`\s*$//;
4049 if ( $self->table_is_allowed($self->{db}, $tbl) ) {4094 if ( $self->_resume_from_table($tbl)
4095 && $self->table_is_allowed($self->{db}, $tbl) ) {
4050 my ($ddl) = $chunk =~ m/^(?:$open_comment)?(CREATE TABLE.+?;)$/ms;4096 my ($ddl) = $chunk =~ m/^(?:$open_comment)?(CREATE TABLE.+?;)$/ms;
4051 if ( !$ddl ) {4097 if ( !$ddl ) {
4052 warn "Failed to parse CREATE TABLE from\n" . $chunk;4098 warn "Failed to parse CREATE TABLE from\n" . $chunk;
4053 next CHUNK;4099 next CHUNK;
4054 }4100 }
4055 $ddl =~ s/ \*\/;\Z/;/; # remove end of version comment4101 $ddl =~ s/ \*\/;\Z/;/; # remove end of version comment
40564102 my $tbl_struct = $self->{TableParser}->parse($ddl);
4057 my ($engine) = $ddl =~ m/\).*?(?:ENGINE|TYPE)=(\w+)/; 4103 if ( $self->engine_is_allowed($tbl_struct->{engine}) ) {
4058
4059 if ( !$engine || $self->engine_is_allowed($engine) ) {
4060 return {4104 return {
4061 db => $self->{db},4105 db => $self->{db},
4062 tbl => $tbl,4106 tbl => $tbl,
4063 ddl => $ddl,4107 name => $self->{Quoter}->quote($self->{db}, $tbl),
4108 ddl => $ddl,
4109 tbl_struct => $tbl_struct,
4064 };4110 };
4065 }4111 }
4066 }4112 }
@@ -4077,6 +4123,7 @@
4077sub _iterate_dbh {4123sub _iterate_dbh {
4078 my ( $self ) = @_;4124 my ( $self ) = @_;
4079 my $q = $self->{Quoter};4125 my $q = $self->{Quoter};
4126 my $tp = $self->{TableParser};
4080 my $dbh = $self->{dbh};4127 my $dbh = $self->{dbh};
4081 PTDEBUG && _d('Getting next schema object from dbh', $dbh);4128 PTDEBUG && _d('Getting next schema object from dbh', $dbh);
40824129
@@ -4090,7 +4137,9 @@
4090 }4137 }
40914138
4092 if ( !$self->{db} ) {4139 if ( !$self->{db} ) {
4093 $self->{db} = shift @{$self->{dbs}};4140 do {
4141 $self->{db} = shift @{$self->{dbs}};
4142 } until $self->_resume_from_database($self->{db});
4094 PTDEBUG && _d('Next database:', $self->{db});4143 PTDEBUG && _d('Next database:', $self->{db});
4095 return unless $self->{db};4144 return unless $self->{db};
4096 }4145 }
@@ -4103,8 +4152,9 @@
4103 }4152 }
4104 grep {4153 grep {
4105 my ($tbl, $type) = @$_;4154 my ($tbl, $type) = @$_;
4106 $self->table_is_allowed($self->{db}, $tbl)4155 (!$type || ($type ne 'VIEW'))
4107 && (!$type || ($type ne 'VIEW'));4156 && $self->_resume_from_table($tbl)
4157 && $self->table_is_allowed($self->{db}, $tbl);
4108 }4158 }
4109 @{$dbh->selectall_arrayref($sql)};4159 @{$dbh->selectall_arrayref($sql)};
4110 PTDEBUG && _d('Found', scalar @tbls, 'tables in database', $self->{db});4160 PTDEBUG && _d('Found', scalar @tbls, 'tables in database', $self->{db});
@@ -4112,27 +4162,15 @@
4112 }4162 }
41134163
4114 while ( my $tbl = shift @{$self->{tbls}} ) {4164 while ( my $tbl = shift @{$self->{tbls}} ) {
4115 my $engine;4165 my $ddl = $tp->get_create_table($dbh, $self->{db}, $tbl);
4116 if ( $self->{filters}->{'engines'}4166 my $tbl_struct = $tp->parse($ddl);
4117 || $self->{filters}->{'ignore-engines'} ) {4167 if ( $self->engine_is_allowed($tbl_struct->{engine}) ) {
4118 my $sql = "SHOW TABLE STATUS FROM " . $q->quote($self->{db})
4119 . " LIKE \'$tbl\'";
4120 PTDEBUG && _d($sql);
4121 $engine = $dbh->selectrow_hashref($sql)->{engine};
4122 PTDEBUG && _d($tbl, 'uses', $engine, 'engine');
4123 }
4124
4125
4126 if ( !$engine || $self->engine_is_allowed($engine) ) {
4127 my $ddl;
4128 if ( my $du = $self->{MySQLDump} ) {
4129 $ddl = $du->get_create_table($dbh, $q, $self->{db}, $tbl)->[1];
4130 }
4131
4132 return {4168 return {
4133 db => $self->{db},4169 db => $self->{db},
4134 tbl => $tbl,4170 tbl => $tbl,
4135 ddl => $ddl,4171 name => $q->quote($self->{db}, $tbl),
4172 ddl => $ddl,
4173 tbl_struct => $tbl_struct,
4136 };4174 };
4137 }4175 }
4138 }4176 }
@@ -4193,6 +4231,10 @@
41934231
4194 my $filter = $self->{filters};4232 my $filter = $self->{filters};
41954233
4234 if ( $db eq 'mysql' && ($tbl eq 'general_log' || $tbl eq 'slow_log') ) {
4235 return 0;
4236 }
4237
4196 if ( $filter->{'ignore-tables'}->{$tbl}4238 if ( $filter->{'ignore-tables'}->{$tbl}
4197 && ($filter->{'ignore-tables'}->{$tbl} eq '*'4239 && ($filter->{'ignore-tables'}->{$tbl} eq '*'
4198 || $filter->{'ignore-tables'}->{$tbl} eq $db) ) {4240 || $filter->{'ignore-tables'}->{$tbl} eq $db) ) {
@@ -4232,7 +4274,11 @@
42324274
4233sub engine_is_allowed {4275sub engine_is_allowed {
4234 my ( $self, $engine ) = @_;4276 my ( $self, $engine ) = @_;
4235 die "I need an engine argument" unless $engine;4277
4278 if ( !$engine ) {
4279 PTDEBUG && _d('No engine specified; allowing the table');
4280 return 1;
4281 }
42364282
4237 $engine = lc $engine;4283 $engine = lc $engine;
42384284
@@ -4252,6 +4298,40 @@
4252 return 1;4298 return 1;
4253}4299}
42544300
4301sub _resume_from_database {
4302 my ($self, $db) = @_;
4303
4304 return 1 unless $self->{resume}->{db};
4305
4306 if ( $db eq $self->{resume}->{db} ) {
4307 PTDEBUG && _d('At resume db', $db);
4308 delete $self->{resume}->{db};
4309 return 1;
4310 }
4311
4312 return 0;
4313}
4314
4315sub _resume_from_table {
4316 my ($self, $tbl) = @_;
4317
4318 return 1 unless $self->{resume}->{tbl};
4319
4320 if ( $tbl eq $self->{resume}->{tbl} ) {
4321 if ( !$self->{resume}->{after} ) {
4322 PTDEBUG && _d('Resuming from table', $tbl);
4323 delete $self->{resume}->{tbl};
4324 return 1;
4325 }
4326 else {
4327 PTDEBUG && _d('Resuming after table', $tbl);
4328 delete $self->{resume}->{tbl};
4329 }
4330 }
4331
4332 return 0;
4333}
4334
4255sub _d {4335sub _d {
4256 my ($package, undef, $line) = caller 0;4336 my ($package, undef, $line) = caller 0;
4257 @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; }4337 @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; }
@@ -5144,13 +5224,11 @@
5144 dbh => $dbh,5224 dbh => $dbh,
5145 OptionParser => $o,5225 OptionParser => $o,
5146 Quoter => $q,5226 Quoter => $q,
5147 MySQLDump => $du,
5148 TableParser => $tp,5227 TableParser => $tp,
5149 Schema => $schema,5228 Schema => $schema,
5150 keep_ddl => 1,
5151 );5229 );
5152 TALBE:5230 TALBE:
5153 while ( my $tbl = $schema_itr->next_schema_object() ) {5231 while ( my $tbl = $schema_itr->next() ) {
5154 eval {5232 eval {
5155 my ($indexes) = $tp->get_keys($tbl->{ddl}, {version => $version});5233 my ($indexes) = $tp->get_keys($tbl->{ddl}, {version => $version});
5156 $iu->add_indexes(%$tbl, indexes=>$indexes);5234 $iu->add_indexes(%$tbl, indexes=>$indexes);
51575235
=== modified file 'bin/pt-table-checksum'
--- bin/pt-table-checksum 2012-03-30 16:10:23 +0000
+++ bin/pt-table-checksum 2012-03-31 16:07:24 +0000
@@ -3941,22 +3941,15 @@
39413941
3942sub get_row_estimate {3942sub get_row_estimate {
3943 my (%args) = @_;3943 my (%args) = @_;
3944 my @required_args = qw(Cxn tbl OptionParser TableParser Quoter);3944 my @required_args = qw(Cxn tbl);
3945 my ($cxn, $tbl, $o, $tp, $q) = @args{@required_args};3945 my ($cxn, $tbl) = @args{@required_args};
39463946
3947 if ( $args{where} ) {3947 my $sql = "EXPLAIN SELECT * FROM $tbl->{name} "
3948 PTDEBUG && _d('WHERE clause, using explain plan for row estimate');3948 . "WHERE " . ($args{where} || '1=1');
3949 my $table = $q->quote(@{$tbl}{qw(db tbl)});3949 PTDEBUG && _d($sql);
3950 my $sql = "EXPLAIN SELECT * FROM $table WHERE $args{where}";3950 my $expl = $cxn->dbh()->selectrow_hashref($sql);
3951 PTDEBUG && _d($sql);3951 PTDEBUG && _d(Dumper($expl));
3952 my $expl = $cxn->dbh()->selectrow_hashref($sql);3952 return ($expl->{rows} || 0), $expl->{key};
3953 PTDEBUG && _d(Dumper($expl));
3954 return ($expl->{rows} || 0), $expl->{key};
3955 }
3956 else {
3957 PTDEBUG && _d('No WHERE clause, using table status for row estimate');
3958 return $tbl->{tbl_status}->{rows} || 0;
3959 }
3960}3953}
39613954
3962sub _prepare_sths {3955sub _prepare_sths {
@@ -4543,7 +4536,7 @@
45434536
4544sub new {4537sub new {
4545 my ( $class, %args ) = @_;4538 my ( $class, %args ) = @_;
4546 my @required_args = qw(OptionParser Quoter);4539 my @required_args = qw(OptionParser TableParser Quoter);
4547 foreach my $arg ( @required_args ) {4540 foreach my $arg ( @required_args ) {
4548 die "I need a $arg argument" unless $args{$arg};4541 die "I need a $arg argument" unless $args{$arg};
4549 }4542 }
@@ -4647,25 +4640,18 @@
4647 }4640 }
46484641
4649 if ( $schema_obj ) {4642 if ( $schema_obj ) {
4650 if ( $schema_obj->{ddl} && $self->{TableParser} ) {
4651 $schema_obj->{tbl_struct}
4652 = $self->{TableParser}->parse($schema_obj->{ddl});
4653 }
4654
4655 delete $schema_obj->{ddl} unless $self->{keep_ddl};
4656 delete $schema_obj->{tbl_status} unless $self->{keep_tbl_status};
4657
4658 if ( my $schema = $self->{Schema} ) {4643 if ( my $schema = $self->{Schema} ) {
4659 $schema->add_schema_object($schema_obj);4644 $schema->add_schema_object($schema_obj);
4660 }4645 }
4661 PTDEBUG && _d('Next schema object:', $schema_obj->{db}, $schema_obj->{tbl});4646 PTDEBUG && _d('Next schema object:',
4647 $schema_obj->{db}, $schema_obj->{tbl});
4662 }4648 }
46634649
4664 return $schema_obj;4650 return $schema_obj;
4665}4651}
46664652
4667sub _iterate_files {4653sub _iterate_files {
4668 my ( $self ) = @_;4654 my ( $self ) = @_;
46694655
4670 if ( !$self->{fh} ) {4656 if ( !$self->{fh} ) {
4671 my ($fh, $file) = $self->{file_itr}->();4657 my ($fh, $file) = $self->{file_itr}->();
@@ -4708,14 +4694,14 @@
4708 next CHUNK;4694 next CHUNK;
4709 }4695 }
4710 $ddl =~ s/ \*\/;\Z/;/; # remove end of version comment4696 $ddl =~ s/ \*\/;\Z/;/; # remove end of version comment
47114697 my $tbl_struct = $self->{TableParser}->parse($ddl);
4712 my ($engine) = $ddl =~ m/\).*?(?:ENGINE|TYPE)=(\w+)/; 4698 if ( $self->engine_is_allowed($tbl_struct->{engine}) ) {
4713
4714 if ( !$engine || $self->engine_is_allowed($engine) ) {
4715 return {4699 return {
4716 db => $self->{db},4700 db => $self->{db},
4717 tbl => $tbl,4701 tbl => $tbl,
4718 ddl => $ddl,4702 name => $self->{Quoter}->quote($self->{db}, $tbl),
4703 ddl => $ddl,
4704 tbl_struct => $tbl_struct,
4719 };4705 };
4720 }4706 }
4721 }4707 }
@@ -4732,6 +4718,7 @@
4732sub _iterate_dbh {4718sub _iterate_dbh {
4733 my ( $self ) = @_;4719 my ( $self ) = @_;
4734 my $q = $self->{Quoter};4720 my $q = $self->{Quoter};
4721 my $tp = $self->{TableParser};
4735 my $dbh = $self->{dbh};4722 my $dbh = $self->{dbh};
4736 PTDEBUG && _d('Getting next schema object from dbh', $dbh);4723 PTDEBUG && _d('Getting next schema object from dbh', $dbh);
47374724
@@ -4770,30 +4757,15 @@
4770 }4757 }
47714758
4772 while ( my $tbl = shift @{$self->{tbls}} ) {4759 while ( my $tbl = shift @{$self->{tbls}} ) {
4773 my $tbl_status;4760 my $ddl = $tp->get_create_table($dbh, $self->{db}, $tbl);
4774 if ( $self->{filters}->{'engines'}4761 my $tbl_struct = $tp->parse($ddl);
4775 || $self->{filters}->{'ignore-engines'}4762 if ( $self->engine_is_allowed($tbl_struct->{engine}) ) {
4776 || $self->{keep_tbl_status} )
4777 {
4778 my $sql = "SHOW TABLE STATUS FROM " . $q->quote($self->{db})
4779 . " LIKE \'$tbl\'";
4780 PTDEBUG && _d($sql);
4781 $tbl_status = $dbh->selectrow_hashref($sql);
4782 PTDEBUG && _d(Dumper($tbl_status));
4783 }
4784
4785 if ( !$tbl_status
4786 || $self->engine_is_allowed($tbl_status->{engine}) ) {
4787 my $ddl;
4788 if ( my $tp = $self->{TableParser} ) {
4789 $ddl = $tp->get_create_table($dbh, $self->{db}, $tbl);
4790 }
4791
4792 return {4763 return {
4793 db => $self->{db},4764 db => $self->{db},
4794 tbl => $tbl,4765 tbl => $tbl,
4766 name => $q->quote($self->{db}, $tbl),
4795 ddl => $ddl,4767 ddl => $ddl,
4796 tbl_status => $tbl_status,4768 tbl_struct => $tbl_struct,
4797 };4769 };
4798 }4770 }
4799 }4771 }
@@ -4897,7 +4869,11 @@
48974869
4898sub engine_is_allowed {4870sub engine_is_allowed {
4899 my ( $self, $engine ) = @_;4871 my ( $self, $engine ) = @_;
4900 die "I need an engine argument" unless $engine;4872
4873 if ( !$engine ) {
4874 PTDEBUG && _d('No engine specified; allowing the table');
4875 return 1;
4876 }
49014877
4902 $engine = lc $engine;4878 $engine = lc $engine;
49034879
@@ -6253,13 +6229,12 @@
6253 }6229 }
62546230
6255 my $schema_iter = new SchemaIterator(6231 my $schema_iter = new SchemaIterator(
6256 dbh => $master_dbh,6232 dbh => $master_dbh,
6257 resume => $last_chunk ? $q->quote(@{$last_chunk}{qw(db tbl)})6233 resume => $last_chunk ? $q->quote(@{$last_chunk}{qw(db tbl)})
6258 : "",6234 : "",
6259 keep_tbl_status => 1,6235 OptionParser => $o,
6260 OptionParser => $o,6236 TableParser => $tp,
6261 TableParser => $tp,6237 Quoter => $q,
6262 Quoter => $q,
6263 );6238 );
62646239
6265 if ( $last_chunk &&6240 if ( $last_chunk &&
@@ -6334,13 +6309,13 @@
6334 my $chunk_size_limit = $o->get('chunk-size-limit');6309 my $chunk_size_limit = $o->get('chunk-size-limit');
6335 my @too_large;6310 my @too_large;
6336 foreach my $slave ( @$slaves ) {6311 foreach my $slave ( @$slaves ) {
6312 # get_row_estimate() returns (row_est, index), but
6313 # we only need the row_est. Maybe in the future we'll
6314 # care what index MySQL will use on a slave.
6337 my ($n_rows) = NibbleIterator::get_row_estimate(6315 my ($n_rows) = NibbleIterator::get_row_estimate(
6338 Cxn => $slave,6316 Cxn => $slave,
6339 tbl => $tbl,6317 tbl => $tbl,
6340 where => $o->get('where') || "1=1",6318 where => $o->get('where'),
6341 OptionParser => $o,
6342 TableParser => $tp,
6343 Quoter => $q,
6344 );6319 );
6345 PTDEBUG && _d('Table on', $slave->name(),6320 PTDEBUG && _d('Table on', $slave->name(),
6346 'has', $n_rows, 'rows');6321 'has', $n_rows, 'rows');
63476322
=== modified file 'bin/pt-table-sync'
--- bin/pt-table-sync 2012-03-30 16:53:51 +0000
+++ bin/pt-table-sync 2012-03-31 16:07:24 +0000
@@ -959,7 +959,7 @@
959 $opt->{value} = ($pre || '') . $num;959 $opt->{value} = ($pre || '') . $num;
960 }960 }
961 else {961 else {
962 $self->save_error("Invalid size for --$opt->{long}");962 $self->save_error("Invalid size for --$opt->{long}: $val");
963 }963 }
964 return;964 return;
965}965}
@@ -1285,12 +1285,14 @@
1285sub as_string {1285sub as_string {
1286 my ( $self, $dsn, $props ) = @_;1286 my ( $self, $dsn, $props ) = @_;
1287 return $dsn unless ref $dsn;1287 return $dsn unless ref $dsn;
1288 my %allowed = $props ? map { $_=>1 } @$props : ();1288 my @keys = $props ? @$props : sort keys %$dsn;
1289 return join(',',1289 return join(',',
1290 map { "$_=" . ($_ eq 'p' ? '...' : $dsn->{$_}) }1290 map { "$_=" . ($_ eq 'p' ? '...' : $dsn->{$_}) }
1291 grep { defined $dsn->{$_} && $self->{opts}->{$_} }1291 grep {
1292 grep { !$props || $allowed{$_} }1292 exists $self->{opts}->{$_}
1293 sort keys %$dsn );1293 && exists $dsn->{$_}
1294 && defined $dsn->{$_}
1295 } @keys);
1294}1296}
12951297
1296sub usage {1298sub usage {
@@ -1741,19 +1743,58 @@
1741 return bless $self, $class;1743 return bless $self, $class;
1742}1744}
17431745
1746sub get_create_table {
1747 my ( $self, $dbh, $db, $tbl ) = @_;
1748 die "I need a dbh parameter" unless $dbh;
1749 die "I need a db parameter" unless $db;
1750 die "I need a tbl parameter" unless $tbl;
1751 my $q = $self->{Quoter};
1752
1753 my $new_sql_mode
1754 = '/*!40101 SET @OLD_SQL_MODE := @@SQL_MODE, '
1755 . q{@@SQL_MODE := REPLACE(REPLACE(@@SQL_MODE, 'ANSI_QUOTES', ''), ',,', ','), }
1756 . '@OLD_QUOTE := @@SQL_QUOTE_SHOW_CREATE, '
1757 . '@@SQL_QUOTE_SHOW_CREATE := 1 */';
1758
1759 my $old_sql_mode = '/*!40101 SET @@SQL_MODE := @OLD_SQL_MODE, '
1760 . '@@SQL_QUOTE_SHOW_CREATE := @OLD_QUOTE */';
1761
1762 PTDEBUG && _d($new_sql_mode);
1763 eval { $dbh->do($new_sql_mode); };
1764 PTDEBUG && $EVAL_ERROR && _d($EVAL_ERROR);
1765
1766 my $use_sql = 'USE ' . $q->quote($db);
1767 PTDEBUG && _d($dbh, $use_sql);
1768 $dbh->do($use_sql);
1769
1770 my $show_sql = "SHOW CREATE TABLE " . $q->quote($db, $tbl);
1771 PTDEBUG && _d($show_sql);
1772 my $href;
1773 eval { $href = $dbh->selectrow_hashref($show_sql); };
1774 if ( $EVAL_ERROR ) {
1775 PTDEBUG && _d($EVAL_ERROR);
1776
1777 PTDEBUG && _d($old_sql_mode);
1778 $dbh->do($old_sql_mode);
1779
1780 return;
1781 }
1782
1783 PTDEBUG && _d($old_sql_mode);
1784 $dbh->do($old_sql_mode);
1785
1786 my ($key) = grep { m/create (?:table|view)/i } keys %$href;
1787 if ( !$key ) {
1788 die "Error: no 'Create Table' or 'Create View' in result set from "
1789 . "$show_sql: " . Dumper($href);
1790 }
1791
1792 return $href->{$key};
1793}
1794
1744sub parse {1795sub parse {
1745 my ( $self, $ddl, $opts ) = @_;1796 my ( $self, $ddl, $opts ) = @_;
1746 return unless $ddl;1797 return unless $ddl;
1747 if ( ref $ddl eq 'ARRAY' ) {
1748 if ( lc $ddl->[0] eq 'table' ) {
1749 $ddl = $ddl->[1];
1750 }
1751 else {
1752 return {
1753 engine => 'VIEW',
1754 };
1755 }
1756 }
17571798
1758 if ( $ddl !~ m/CREATE (?:TEMPORARY )?TABLE `/ ) {1799 if ( $ddl !~ m/CREATE (?:TEMPORARY )?TABLE `/ ) {
1759 die "Cannot parse table definition; is ANSI quoting "1800 die "Cannot parse table definition; is ANSI quoting "
@@ -2060,41 +2101,31 @@
2060 return $ddl;2101 return $ddl;
2061}2102}
20622103
2063sub remove_secondary_indexes {2104sub get_table_status {
2064 my ( $self, $ddl ) = @_;2105 my ( $self, $dbh, $db, $like ) = @_;
2065 my $sec_indexes_ddl;2106 my $q = $self->{Quoter};
2066 my $tbl_struct = $self->parse($ddl);2107 my $sql = "SHOW TABLE STATUS FROM " . $q->quote($db);
20672108 my @params;
2068 if ( ($tbl_struct->{engine} || '') =~ m/InnoDB/i ) {2109 if ( $like ) {
2069 my $clustered_key = $tbl_struct->{clustered_key};2110 $sql .= ' LIKE ?';
2070 $clustered_key ||= '';2111 push @params, $like;
20712112 }
2072 my @sec_indexes = map {2113 PTDEBUG && _d($sql, @params);
2073 my $key_def = $_->{ddl};2114 my $sth = $dbh->prepare($sql);
2074 $key_def =~ s/([\(\)])/\\$1/g;2115 eval { $sth->execute(@params); };
2075 $ddl =~ s/\s+$key_def//i;2116 if ($EVAL_ERROR) {
20762117 PTDEBUG && _d($EVAL_ERROR);
2077 my $key_ddl = "ADD $_->{ddl}";2118 return;
2078 $key_ddl .= ',' unless $key_ddl =~ m/,$/;2119 }
2079 $key_ddl;2120 my @tables = @{$sth->fetchall_arrayref({})};
2080 }2121 @tables = map {
2081 grep { $_->{name} ne $clustered_key }2122 my %tbl; # Make a copy with lowercased keys
2082 values %{$tbl_struct->{keys}};2123 @tbl{ map { lc $_ } keys %$_ } = values %$_;
2083 PTDEBUG && _d('Secondary indexes:', Dumper(\@sec_indexes));2124 $tbl{engine} ||= $tbl{type} || $tbl{comment};
20842125 delete $tbl{type};
2085 if ( @sec_indexes ) {2126 \%tbl;
2086 $sec_indexes_ddl = join(' ', @sec_indexes);2127 } @tables;
2087 $sec_indexes_ddl =~ s/,$//;2128 return @tables;
2088 }
2089
2090 $ddl =~ s/,(\n\) )/$1/s;
2091 }
2092 else {
2093 PTDEBUG && _d('Not removing secondary indexes from',
2094 $tbl_struct->{engine}, 'table');
2095 }
2096
2097 return $ddl, $sec_indexes_ddl, $tbl_struct;
2098}2129}
20992130
2100sub _d {2131sub _d {
@@ -5445,11 +5476,12 @@
54455476
5446 eval {5477 eval {
5447 if ( my $timeout = $args{wait} ) {5478 if ( my $timeout = $args{wait} ) {
5448 my $wait = $args{wait_retry_args}->{wait} || 10;5479 my $ms = $self->{MasterSlave};
5449 my $tries = $args{wait_retry_args}->{tries} || 3;5480 my $tries = $args{wait_retry_args}->{tries} || 3;
5481 my $wait;
5450 $self->{Retry}->retry(5482 $self->{Retry}->retry(
5451 wait => sub { sleep $wait; },
5452 tries => $tries,5483 tries => $tries,
5484 wait => sub { sleep $args{wait_retry_args}->{wait} || 10 },
5453 try => sub {5485 try => sub {
5454 my ( %args ) = @_;5486 my ( %args ) = @_;
54555487
@@ -5457,12 +5489,18 @@
5457 warn "Retrying MASTER_POS_WAIT() for --wait $timeout...";5489 warn "Retrying MASTER_POS_WAIT() for --wait $timeout...";
5458 }5490 }
54595491
5460 my $ms = $self->{MasterSlave};5492 $wait = $ms->wait_for_master(
5461 my $wait = $ms->wait_for_master(
5462 master_status => $ms->get_master_status($src->{misc_dbh}),5493 master_status => $ms->get_master_status($src->{misc_dbh}),
5463 slave_dbh => $dst->{dbh},5494 slave_dbh => $dst->{dbh},
5464 timeout => $timeout,5495 timeout => $timeout,
5465 );5496 );
5497 if ( defined $wait->{result} && $wait->{result} != -1 ) {
5498 return; # slave caught up
5499 }
5500 die; # call fail
5501 },
5502 fail => sub {
5503 my (%args) = @_;
5466 if ( !defined $wait->{result} ) {5504 if ( !defined $wait->{result} ) {
5467 my $msg;5505 my $msg;
5468 if ( $wait->{waited} ) {5506 if ( $wait->{waited} ) {
@@ -5477,20 +5515,14 @@
5477 $msg .= " Sleeping $wait seconds then retrying "5515 $msg .= " Sleeping $wait seconds then retrying "
5478 . ($tries - $args{tryno}) . " more times.";5516 . ($tries - $args{tryno}) . " more times.";
5479 }5517 }
5480 warn $msg;5518 warn "$msg\n";
5481 return;5519 return 1; # call wait, call try
5482 }5520 }
5483 elsif ( $wait->{result} == -1 ) {5521 elsif ( $wait->{result} == -1 ) {
5484 die "Slave did not catch up to its master after waiting "5522 return 0; # call final_fail
5485 . "$timeout seconds with MASTER_POS_WAIT. Try inceasing "
5486 . "the --wait time, or disable this feature by specifying "
5487 . "--wait 0.";
5488 }
5489 else {
5490 return $result; # slave caught up
5491 }5523 }
5492 },5524 },
5493 on_failure => sub {5525 final_fail => sub {
5494 die "Slave did not catch up to its master after $tries attempts "5526 die "Slave did not catch up to its master after $tries attempts "
5495 . "of waiting $timeout seconds with MASTER_POS_WAIT. "5527 . "of waiting $timeout seconds with MASTER_POS_WAIT. "
5496 . "Check that the slave is running, increase the --wait "5528 . "Check that the slave is running, increase the --wait "
@@ -5603,23 +5635,21 @@
5603 die "I need a $arg argument" unless defined $args{$arg};5635 die "I need a $arg argument" unless defined $args{$arg};
5604 }5636 }
5605 my ($tbl_struct, $index) = @args{@required_args};5637 my ($tbl_struct, $index) = @args{@required_args};
5606 my @cols = $args{cols} ? @{$args{cols}} : @{$tbl_struct->{cols}};5638 my @cols = $args{cols} ? @{$args{cols}} : @{$tbl_struct->{cols}};
5607 my $q = $self->{Quoter};5639 my $q = $self->{Quoter};
56085640
5609 die "Index '$index' does not exist in table"5641 die "Index '$index' does not exist in table"
5610 unless exists $tbl_struct->{keys}->{$index};5642 unless exists $tbl_struct->{keys}->{$index};
5643 PTDEBUG && _d('Will ascend index', $index);
56115644
5612 my @asc_cols = @{$tbl_struct->{keys}->{$index}->{cols}};5645 my @asc_cols = @{$tbl_struct->{keys}->{$index}->{cols}};
5613 my @asc_slice;
5614
5615 @asc_cols = @{$tbl_struct->{keys}->{$index}->{cols}};
5616 PTDEBUG && _d('Will ascend index', $index);
5617 PTDEBUG && _d('Will ascend columns', join(', ', @asc_cols));
5618 if ( $args{asc_first} ) {5646 if ( $args{asc_first} ) {
5619 @asc_cols = $asc_cols[0];5647 @asc_cols = $asc_cols[0];
5620 PTDEBUG && _d('Ascending only first column');5648 PTDEBUG && _d('Ascending only first column');
5621 }5649 }
5650 PTDEBUG && _d('Will ascend columns', join(', ', @asc_cols));
56225651
5652 my @asc_slice;
5623 my %col_posn = do { my $i = 0; map { $_ => $i++ } @cols };5653 my %col_posn = do { my $i = 0; map { $_ => $i++ } @cols };
5624 foreach my $col ( @asc_cols ) {5654 foreach my $col ( @asc_cols ) {
5625 if ( !exists $col_posn{$col} ) {5655 if ( !exists $col_posn{$col} ) {
@@ -6720,292 +6750,427 @@
6720# ###########################################################################6750# ###########################################################################
67216751
6722# ###########################################################################6752# ###########################################################################
6723# SchemaIterator r71416753# SchemaIterator package
6724# Don't update this package!6754# This package is a copy without comments from the original. The original
6755# with comments and its test file can be found in the Bazaar repository at,
6756# lib/SchemaIterator.pm
6757# t/lib/SchemaIterator.t
6758# See https://launchpad.net/percona-toolkit for more information.
6725# ###########################################################################6759# ###########################################################################
6760{
6726package SchemaIterator;6761package SchemaIterator;
67276762
6728use strict;6763use strict;
6729use warnings FATAL => 'all';6764use warnings FATAL => 'all';
6730
6731use English qw(-no_match_vars);6765use English qw(-no_match_vars);
6766use constant PTDEBUG => $ENV{PTDEBUG} || 0;
6767
6732use Data::Dumper;6768use Data::Dumper;
6733$Data::Dumper::Indent = 1;6769$Data::Dumper::Indent = 1;
6734$Data::Dumper::Sortkeys = 1;6770$Data::Dumper::Sortkeys = 1;
6735$Data::Dumper::Quotekeys = 0;6771$Data::Dumper::Quotekeys = 0;
67366772
6737use constant PTDEBUG => $ENV{PTDEBUG} || 0;6773my $open_comment = qr{/\*!\d{5} };
6774my $tbl_name = qr{
6775 CREATE\s+
6776 (?:TEMPORARY\s+)?
6777 TABLE\s+
6778 (?:IF NOT EXISTS\s+)?
6779 ([^\(]+)
6780}x;
6781
67386782
6739sub new {6783sub new {
6740 my ( $class, %args ) = @_;6784 my ( $class, %args ) = @_;
6741 foreach my $arg ( qw(Quoter) ) {6785 my @required_args = qw(OptionParser TableParser Quoter);
6786 foreach my $arg ( @required_args ) {
6742 die "I need a $arg argument" unless $args{$arg};6787 die "I need a $arg argument" unless $args{$arg};
6743 }6788 }
6789
6790 my ($file_itr, $dbh) = @args{qw(file_itr dbh)};
6791 die "I need either a dbh or file_itr argument"
6792 if (!$dbh && !$file_itr) || ($dbh && $file_itr);
6793
6794 my %resume;
6795 if ( my $table = $args{resume} ) {
6796 PTDEBUG && _d('Will resume from or after', $table);
6797 my ($db, $tbl) = $args{Quoter}->split_unquote($table);
6798 die "Resume table must be database-qualified: $table"
6799 unless $db && $tbl;
6800 $resume{db} = $db;
6801 $resume{tbl} = $tbl;
6802 }
6803
6744 my $self = {6804 my $self = {
6745 %args,6805 %args,
6746 filter => undef,6806 resume => \%resume,
6747 dbs => [],6807 filters => _make_filters(%args),
6748 };6808 };
6809
6749 return bless $self, $class;6810 return bless $self, $class;
6750}6811}
67516812
6752sub make_filter {6813sub _make_filters {
6753 my ( $self, $o ) = @_;6814 my ( %args ) = @_;
6754 my @lines = (6815 my @required_args = qw(OptionParser Quoter);
6755 'sub {',
6756 ' my ( $dbh, $db, $tbl ) = @_;',
6757 ' my $engine = undef;',
6758 );
6759
6760
6761 my @permit_dbs = _make_filter('unless', '$db', $o->get('databases'))
6762 if $o->has('databases');
6763 my @reject_dbs = _make_filter('if', '$db', $o->get('ignore-databases'))
6764 if $o->has('ignore-databases');
6765 my @dbs_regex;
6766 if ( $o->has('databases-regex') && (my $p = $o->get('databases-regex')) ) {
6767 push @dbs_regex, " return 0 unless \$db && (\$db =~ m/$p/o);";
6768 }
6769 my @reject_dbs_regex;
6770 if ( $o->has('ignore-databases-regex')
6771 && (my $p = $o->get('ignore-databases-regex')) ) {
6772 push @reject_dbs_regex, " return 0 if \$db && (\$db =~ m/$p/o);";
6773 }
6774 if ( @permit_dbs || @reject_dbs || @dbs_regex || @reject_dbs_regex ) {
6775 push @lines,
6776 ' if ( $db ) {',
6777 (@permit_dbs ? @permit_dbs : ()),
6778 (@reject_dbs ? @reject_dbs : ()),
6779 (@dbs_regex ? @dbs_regex : ()),
6780 (@reject_dbs_regex ? @reject_dbs_regex : ()),
6781 ' }';
6782 }
6783
6784 if ( $o->has('tables') || $o->has('ignore-tables')
6785 || $o->has('ignore-tables-regex') ) {
6786
6787 my $have_qtbl = 0;
6788 my $have_only_qtbls = 0;
6789 my %qtbls;
6790
6791 my @permit_tbls;
6792 my @permit_qtbls;
6793 my %permit_qtbls;
6794 if ( $o->get('tables') ) {
6795 my %tbls;
6796 map {
6797 if ( $_ =~ m/\./ ) {
6798 $permit_qtbls{$_} = 1;
6799 }
6800 else {
6801 $tbls{$_} = 1;
6802 }
6803 } keys %{ $o->get('tables') };
6804 @permit_tbls = _make_filter('unless', '$tbl', \%tbls);
6805 @permit_qtbls = _make_filter('unless', '$qtbl', \%permit_qtbls);
6806
6807 if ( @permit_qtbls ) {
6808 push @lines,
6809 ' my $qtbl = ($db ? "$db." : "") . ($tbl ? $tbl : "");';
6810 $have_qtbl = 1;
6811 }
6812 }
6813
6814 my @reject_tbls;
6815 my @reject_qtbls;
6816 my %reject_qtbls;
6817 if ( $o->get('ignore-tables') ) {
6818 my %tbls;
6819 map {
6820 if ( $_ =~ m/\./ ) {
6821 $reject_qtbls{$_} = 1;
6822 }
6823 else {
6824 $tbls{$_} = 1;
6825 }
6826 } keys %{ $o->get('ignore-tables') };
6827 @reject_tbls= _make_filter('if', '$tbl', \%tbls);
6828 @reject_qtbls = _make_filter('if', '$qtbl', \%reject_qtbls);
6829
6830 if ( @reject_qtbls && !$have_qtbl ) {
6831 push @lines,
6832 ' my $qtbl = ($db ? "$db." : "") . ($tbl ? $tbl : "");';
6833 }
6834 }
6835
6836 if ( keys %permit_qtbls && !@permit_dbs ) {
6837 my $dbs = {};
6838 map {
6839 my ($db, undef) = split(/\./, $_);
6840 $dbs->{$db} = 1;
6841 } keys %permit_qtbls;
6842 PTDEBUG && _d('Adding restriction "--databases',
6843 (join(',', keys %$dbs) . '"'));
6844 if ( keys %$dbs ) {
6845 $o->set('databases', $dbs);
6846 return $self->make_filter($o);
6847 }
6848 }
6849
6850 my @tbls_regex;
6851 if ( $o->has('tables-regex') && (my $p = $o->get('tables-regex')) ) {
6852 push @tbls_regex, " return 0 unless \$tbl && (\$tbl =~ m/$p/o);";
6853 }
6854 my @reject_tbls_regex;
6855 if ( $o->has('ignore-tables-regex')
6856 && (my $p = $o->get('ignore-tables-regex')) ) {
6857 push @reject_tbls_regex,
6858 " return 0 if \$tbl && (\$tbl =~ m/$p/o);";
6859 }
6860
6861 my @get_eng;
6862 my @permit_engs;
6863 my @reject_engs;
6864 if ( ($o->has('engines') && $o->get('engines'))
6865 || ($o->has('ignore-engines') && $o->get('ignore-engines')) ) {
6866 push @get_eng,
6867 ' my $sql = "SHOW TABLE STATUS "',
6868 ' . ($db ? "FROM `$db`" : "")',
6869 ' . " LIKE \'$tbl\'";',
6870 ' PTDEBUG && _d($sql);',
6871 ' eval {',
6872 ' $engine = $dbh->selectrow_hashref($sql)->{engine};',
6873 ' };',
6874 ' PTDEBUG && $EVAL_ERROR && _d($EVAL_ERROR);',
6875 ' PTDEBUG && _d($tbl, "uses engine", $engine);',
6876 ' $engine = lc $engine if $engine;',
6877 @permit_engs
6878 = _make_filter('unless', '$engine', $o->get('engines'), 1);
6879 @reject_engs
6880 = _make_filter('if', '$engine', $o->get('ignore-engines'), 1)
6881 }
6882
6883 if ( @permit_tbls || @permit_qtbls || @reject_tbls || @tbls_regex
6884 || @reject_tbls_regex || @permit_engs || @reject_engs ) {
6885 push @lines,
6886 ' if ( $tbl ) {',
6887 (@permit_tbls ? @permit_tbls : ()),
6888 (@reject_tbls ? @reject_tbls : ()),
6889 (@tbls_regex ? @tbls_regex : ()),
6890 (@reject_tbls_regex ? @reject_tbls_regex : ()),
6891 (@permit_qtbls ? @permit_qtbls : ()),
6892 (@reject_qtbls ? @reject_qtbls : ()),
6893 (@get_eng ? @get_eng : ()),
6894 (@permit_engs ? @permit_engs : ()),
6895 (@reject_engs ? @reject_engs : ()),
6896 ' }';
6897 }
6898 }
6899
6900 push @lines,
6901 ' PTDEBUG && _d(\'Passes filters:\', $db, $tbl, $engine, $dbh);',
6902 ' return 1;', '}';
6903
6904 my $code = join("\n", @lines);
6905 PTDEBUG && _d('filter sub:', $code);
6906 my $filter_sub= eval $code
6907 or die "Error compiling subroutine code:\n$code\n$EVAL_ERROR";
6908
6909 return $filter_sub;
6910}
6911
6912sub set_filter {
6913 my ( $self, $filter_sub ) = @_;
6914 $self->{filter} = $filter_sub;
6915 PTDEBUG && _d('Set filter sub');
6916 return;
6917}
6918
6919sub get_db_itr {
6920 my ( $self, %args ) = @_;
6921 my @required_args = qw(dbh);
6922 foreach my $arg ( @required_args ) {6816 foreach my $arg ( @required_args ) {
6923 die "I need a $arg argument" unless $args{$arg};6817 die "I need a $arg argument" unless $args{$arg};
6924 }6818 }
6925 my ($dbh) = @args{@required_args};6819 my ($o, $q) = @args{@required_args};
69266820
6927 my $filter = $self->{filter};6821 my %filters;
6928 my @dbs;6822
6929 eval {6823
6824 my @simple_filters = qw(
6825 databases tables engines
6826 ignore-databases ignore-tables ignore-engines);
6827 FILTER:
6828 foreach my $filter ( @simple_filters ) {
6829 if ( $o->has($filter) ) {
6830 my $objs = $o->get($filter);
6831 next FILTER unless $objs && scalar keys %$objs;
6832 my $is_table = $filter =~ m/table/ ? 1 : 0;
6833 foreach my $obj ( keys %$objs ) {
6834 die "Undefined value for --$filter" unless $obj;
6835 $obj = lc $obj;
6836 if ( $is_table ) {
6837 my ($db, $tbl) = $q->split_unquote($obj);
6838 $db ||= '*';
6839 PTDEBUG && _d('Filter', $filter, 'value:', $db, $tbl);
6840 $filters{$filter}->{$tbl} = $db;
6841 }
6842 else { # database
6843 PTDEBUG && _d('Filter', $filter, 'value:', $obj);
6844 $filters{$filter}->{$obj} = 1;
6845 }
6846 }
6847 }
6848 }
6849
6850 my @regex_filters = qw(
6851 databases-regex tables-regex
6852 ignore-databases-regex ignore-tables-regex);
6853 REGEX_FILTER:
6854 foreach my $filter ( @regex_filters ) {
6855 if ( $o->has($filter) ) {
6856 my $pat = $o->get($filter);
6857 next REGEX_FILTER unless $pat;
6858 $filters{$filter} = qr/$pat/;
6859 PTDEBUG && _d('Filter', $filter, 'value:', $filters{$filter});
6860 }
6861 }
6862
6863 PTDEBUG && _d('Schema object filters:', Dumper(\%filters));
6864 return \%filters;
6865}
6866
6867sub next {
6868 my ( $self ) = @_;
6869
6870 if ( !$self->{initialized} ) {
6871 $self->{initialized} = 1;
6872 if ( $self->{resume}->{tbl}
6873 && !$self->table_is_allowed(@{$self->{resume}}{qw(db tbl)}) ) {
6874 PTDEBUG && _d('Will resume after',
6875 join('.', @{$self->{resume}}{qw(db tbl)}));
6876 $self->{resume}->{after} = 1;
6877 }
6878 }
6879
6880 my $schema_obj;
6881 if ( $self->{file_itr} ) {
6882 $schema_obj= $self->_iterate_files();
6883 }
6884 else { # dbh
6885 $schema_obj= $self->_iterate_dbh();
6886 }
6887
6888 if ( $schema_obj ) {
6889 if ( my $schema = $self->{Schema} ) {
6890 $schema->add_schema_object($schema_obj);
6891 }
6892 PTDEBUG && _d('Next schema object:',
6893 $schema_obj->{db}, $schema_obj->{tbl});
6894 }
6895
6896 return $schema_obj;
6897}
6898
6899sub _iterate_files {
6900 my ( $self ) = @_;
6901
6902 if ( !$self->{fh} ) {
6903 my ($fh, $file) = $self->{file_itr}->();
6904 if ( !$fh ) {
6905 PTDEBUG && _d('No more files to iterate');
6906 return;
6907 }
6908 $self->{fh} = $fh;
6909 $self->{file} = $file;
6910 }
6911 my $fh = $self->{fh};
6912 PTDEBUG && _d('Getting next schema object from', $self->{file});
6913
6914 local $INPUT_RECORD_SEPARATOR = '';
6915 CHUNK:
6916 while (defined(my $chunk = <$fh>)) {
6917 if ($chunk =~ m/Database: (\S+)/) {
6918 my $db = $1; # XXX
6919 $db =~ s/^`//; # strip leading `
6920 $db =~ s/`$//; # and trailing `
6921 if ( $self->database_is_allowed($db)
6922 && $self->_resume_from_database($db) ) {
6923 $self->{db} = $db;
6924 }
6925 }
6926 elsif ($self->{db} && $chunk =~ m/CREATE TABLE/) {
6927 if ($chunk =~ m/DROP VIEW IF EXISTS/) {
6928 PTDEBUG && _d('Table is a VIEW, skipping');
6929 next CHUNK;
6930 }
6931
6932 my ($tbl) = $chunk =~ m/$tbl_name/;
6933 $tbl =~ s/^\s*`//;
6934 $tbl =~ s/`\s*$//;
6935 if ( $self->_resume_from_table($tbl)
6936 && $self->table_is_allowed($self->{db}, $tbl) ) {
6937 my ($ddl) = $chunk =~ m/^(?:$open_comment)?(CREATE TABLE.+?;)$/ms;
6938 if ( !$ddl ) {
6939 warn "Failed to parse CREATE TABLE from\n" . $chunk;
6940 next CHUNK;
6941 }
6942 $ddl =~ s/ \*\/;\Z/;/; # remove end of version comment
6943 my $tbl_struct = $self->{TableParser}->parse($ddl);
6944 if ( $self->engine_is_allowed($tbl_struct->{engine}) ) {
6945 return {
6946 db => $self->{db},
6947 tbl => $tbl,
6948 name => $self->{Quoter}->quote($self->{db}, $tbl),
6949 ddl => $ddl,
6950 tbl_struct => $tbl_struct,
6951 };
6952 }
6953 }
6954 }
6955 } # CHUNK
6956
6957 PTDEBUG && _d('No more schema objects in', $self->{file});
6958 close $self->{fh};
6959 $self->{fh} = undef;
6960
6961 return $self->_iterate_files();
6962}
6963
6964sub _iterate_dbh {
6965 my ( $self ) = @_;
6966 my $q = $self->{Quoter};
6967 my $tp = $self->{TableParser};
6968 my $dbh = $self->{dbh};
6969 PTDEBUG && _d('Getting next schema object from dbh', $dbh);
6970
6971 if ( !defined $self->{dbs} ) {
6930 my $sql = 'SHOW DATABASES';6972 my $sql = 'SHOW DATABASES';
6931 PTDEBUG && _d($sql);6973 PTDEBUG && _d($sql);
6932 @dbs = grep {6974 my @dbs = grep { $self->database_is_allowed($_) }
6933 my $ok = $filter ? $filter->($dbh, $_, undef) : 1;6975 @{$dbh->selectcol_arrayref($sql)};
6934 $ok = 0 if $_ =~ m/information_schema|performance_schema|lost\+found/;
6935 $ok;
6936 } @{ $dbh->selectcol_arrayref($sql) };
6937 PTDEBUG && _d('Found', scalar @dbs, 'databases');6976 PTDEBUG && _d('Found', scalar @dbs, 'databases');
6938 };6977 $self->{dbs} = \@dbs;
69396978 }
6940 PTDEBUG && $EVAL_ERROR && _d($EVAL_ERROR);6979
6941 my $iterator = sub {6980 if ( !$self->{db} ) {
6942 return shift @dbs;6981 do {
6943 };6982 $self->{db} = shift @{$self->{dbs}};
69446983 } until $self->_resume_from_database($self->{db});
6945 if (wantarray) {6984 PTDEBUG && _d('Next database:', $self->{db});
6946 return ($iterator, scalar @dbs);6985 return unless $self->{db};
6947 }6986 }
6948 else {6987
6949 return $iterator;6988 if ( !defined $self->{tbls} ) {
6950 }6989 my $sql = 'SHOW /*!50002 FULL*/ TABLES FROM ' . $q->quote($self->{db});
6951}6990 PTDEBUG && _d($sql);
69526991 my @tbls = map {
6953sub get_tbl_itr {6992 $_->[0]; # (tbl, type)
6954 my ( $self, %args ) = @_;6993 }
6955 my @required_args = qw(dbh db);6994 grep {
6956 foreach my $arg ( @required_args ) {6995 my ($tbl, $type) = @$_;
6957 die "I need a $arg argument" unless $args{$arg};6996 (!$type || ($type ne 'VIEW'))
6958 }6997 && $self->_resume_from_table($tbl)
6959 my ($dbh, $db, $views) = @args{@required_args, 'views'};6998 && $self->table_is_allowed($self->{db}, $tbl);
69606999 }
6961 my $filter = $self->{filter};7000 @{$dbh->selectall_arrayref($sql)};
6962 my @tbls;7001 PTDEBUG && _d('Found', scalar @tbls, 'tables in database', $self->{db});
6963 if ( $db ) {7002 $self->{tbls} = \@tbls;
6964 eval {7003 }
6965 my $sql = 'SHOW /*!50002 FULL*/ TABLES FROM '7004
6966 . $self->{Quoter}->quote($db);7005 while ( my $tbl = shift @{$self->{tbls}} ) {
6967 PTDEBUG && _d($sql);7006 my $ddl = $tp->get_create_table($dbh, $self->{db}, $tbl);
6968 @tbls = map {7007 my $tbl_struct = $tp->parse($ddl);
6969 $_->[0]7008 if ( $self->engine_is_allowed($tbl_struct->{engine}) ) {
6970 }7009 return {
6971 grep {7010 db => $self->{db},
6972 my ($tbl, $type) = @$_;7011 tbl => $tbl,
6973 my $ok = $filter ? $filter->($dbh, $db, $tbl) : 1;7012 name => $q->quote($self->{db}, $tbl),
6974 if ( !$views ) {7013 ddl => $ddl,
6975 $ok = 0 if ($type || '') eq 'VIEW';7014 tbl_struct => $tbl_struct,
6976 }7015 };
6977 $ok;7016 }
6978 }7017 }
6979 @{ $dbh->selectall_arrayref($sql) };7018
6980 PTDEBUG && _d('Found', scalar @tbls, 'tables in', $db);7019 PTDEBUG && _d('No more tables in database', $self->{db});
6981 };7020 $self->{db} = undef;
6982 PTDEBUG && $EVAL_ERROR && _d($EVAL_ERROR);7021 $self->{tbls} = undef;
6983 }7022
6984 else {7023 return $self->_iterate_dbh();
6985 PTDEBUG && _d('No db given so no tables');7024}
6986 }7025
69877026sub database_is_allowed {
6988 my $iterator = sub {7027 my ( $self, $db ) = @_;
6989 return shift @tbls;7028 die "I need a db argument" unless $db;
6990 };7029
69917030 $db = lc $db;
6992 if ( wantarray ) {7031
6993 return ($iterator, scalar @tbls);7032 my $filter = $self->{filters};
6994 }7033
6995 else {7034 if ( $db =~ m/information_schema|performance_schema|lost\+found/ ) {
6996 return $iterator;7035 PTDEBUG && _d('Database', $db, 'is a system database, ignoring');
6997 }7036 return 0;
6998}7037 }
69997038
7000sub _make_filter {7039 if ( $self->{filters}->{'ignore-databases'}->{$db} ) {
7001 my ( $cond, $var_name, $objs, $lc ) = @_;7040 PTDEBUG && _d('Database', $db, 'is in --ignore-databases list');
7002 my @lines;7041 return 0;
7003 if ( scalar keys %$objs ) {7042 }
7004 my $test = join(' || ',7043
7005 map { "$var_name eq '" . ($lc ? lc $_ : $_) ."'" } keys %$objs);7044 if ( $filter->{'ignore-databases-regex'}
7006 push @lines, " return 0 $cond $var_name && ($test);",7045 && $db =~ $filter->{'ignore-databases-regex'} ) {
7007 }7046 PTDEBUG && _d('Database', $db, 'matches --ignore-databases-regex');
7008 return @lines;7047 return 0;
7048 }
7049
7050 if ( $filter->{'databases'}
7051 && !$filter->{'databases'}->{$db} ) {
7052 PTDEBUG && _d('Database', $db, 'is not in --databases list, ignoring');
7053 return 0;
7054 }
7055
7056 if ( $filter->{'databases-regex'}
7057 && $db !~ $filter->{'databases-regex'} ) {
7058 PTDEBUG && _d('Database', $db, 'does not match --databases-regex, ignoring');
7059 return 0;
7060 }
7061
7062 return 1;
7063}
7064
7065sub table_is_allowed {
7066 my ( $self, $db, $tbl ) = @_;
7067 die "I need a db argument" unless $db;
7068 die "I need a tbl argument" unless $tbl;
7069
7070 $db = lc $db;
7071 $tbl = lc $tbl;
7072
7073 my $filter = $self->{filters};
7074
7075 if ( $db eq 'mysql' && ($tbl eq 'general_log' || $tbl eq 'slow_log') ) {
7076 return 0;
7077 }
7078
7079 if ( $filter->{'ignore-tables'}->{$tbl}
7080 && ($filter->{'ignore-tables'}->{$tbl} eq '*'
7081 || $filter->{'ignore-tables'}->{$tbl} eq $db) ) {
7082 PTDEBUG && _d('Table', $tbl, 'is in --ignore-tables list');
7083 return 0;
7084 }
7085
7086 if ( $filter->{'ignore-tables-regex'}
7087 && $tbl =~ $filter->{'ignore-tables-regex'} ) {
7088 PTDEBUG && _d('Table', $tbl, 'matches --ignore-tables-regex');
7089 return 0;
7090 }
7091
7092 if ( $filter->{'tables'}
7093 && !$filter->{'tables'}->{$tbl} ) {
7094 PTDEBUG && _d('Table', $tbl, 'is not in --tables list, ignoring');
7095 return 0;
7096 }
7097
7098 if ( $filter->{'tables-regex'}
7099 && $tbl !~ $filter->{'tables-regex'} ) {
7100 PTDEBUG && _d('Table', $tbl, 'does not match --tables-regex, ignoring');
7101 return 0;
7102 }
7103
7104 if ( $filter->{'tables'}
7105 && $filter->{'tables'}->{$tbl}
7106 && $filter->{'tables'}->{$tbl} ne '*'
7107 && $filter->{'tables'}->{$tbl} ne $db ) {
7108 PTDEBUG && _d('Table', $tbl, 'is only allowed in database',
7109 $filter->{'tables'}->{$tbl});
7110 return 0;
7111 }
7112
7113 return 1;
7114}
7115
7116sub engine_is_allowed {
7117 my ( $self, $engine ) = @_;
7118
7119 if ( !$engine ) {
7120 PTDEBUG && _d('No engine specified; allowing the table');
7121 return 1;
7122 }
7123
7124 $engine = lc $engine;
7125
7126 my $filter = $self->{filters};
7127
7128 if ( $filter->{'ignore-engines'}->{$engine} ) {
7129 PTDEBUG && _d('Engine', $engine, 'is in --ignore-databases list');
7130 return 0;
7131 }
7132
7133 if ( $filter->{'engines'}
7134 && !$filter->{'engines'}->{$engine} ) {
7135 PTDEBUG && _d('Engine', $engine, 'is not in --engines list, ignoring');
7136 return 0;
7137 }
7138
7139 return 1;
7140}
7141
7142sub _resume_from_database {
7143 my ($self, $db) = @_;
7144
7145 return 1 unless $self->{resume}->{db};
7146
7147 if ( $db eq $self->{resume}->{db} ) {
7148 PTDEBUG && _d('At resume db', $db);
7149 delete $self->{resume}->{db};
7150 return 1;
7151 }
7152
7153 return 0;
7154}
7155
7156sub _resume_from_table {
7157 my ($self, $tbl) = @_;
7158
7159 return 1 unless $self->{resume}->{tbl};
7160
7161 if ( $tbl eq $self->{resume}->{tbl} ) {
7162 if ( !$self->{resume}->{after} ) {
7163 PTDEBUG && _d('Resuming from table', $tbl);
7164 delete $self->{resume}->{tbl};
7165 return 1;
7166 }
7167 else {
7168 PTDEBUG && _d('Resuming after table', $tbl);
7169 delete $self->{resume}->{tbl};
7170 }
7171 }
7172
7173 return 0;
7009}7174}
70107175
7011sub _d {7176sub _d {
@@ -7017,7 +7182,7 @@
7017}7182}
70187183
70191;71841;
70207185}
7021# ###########################################################################7186# ###########################################################################
7022# End SchemaIterator package7187# End SchemaIterator package
7023# ###########################################################################7188# ###########################################################################
@@ -7304,48 +7469,42 @@
73047469
7305sub retry {7470sub retry {
7306 my ( $self, %args ) = @_;7471 my ( $self, %args ) = @_;
7307 my @required_args = qw(try wait);7472 my @required_args = qw(try fail final_fail);
7308 foreach my $arg ( @required_args ) {7473 foreach my $arg ( @required_args ) {
7309 die "I need a $arg argument" unless $args{$arg};7474 die "I need a $arg argument" unless $args{$arg};
7310 };7475 };
7311 my ($try, $wait) = @args{@required_args};7476 my ($try, $fail, $final_fail) = @args{@required_args};
7477 my $wait = $args{wait} || sub { sleep 1; };
7312 my $tries = $args{tries} || 3;7478 my $tries = $args{tries} || 3;
73137479
7480 my $last_error;
7314 my $tryno = 0;7481 my $tryno = 0;
7482 TRY:
7315 while ( ++$tryno <= $tries ) {7483 while ( ++$tryno <= $tries ) {
7316 PTDEBUG && _d("Retry", $tryno, "of", $tries);7484 PTDEBUG && _d("Try", $tryno, "of", $tries);
7317 my $result;7485 my $result;
7318 eval {7486 eval {
7319 $result = $try->(tryno=>$tryno);7487 $result = $try->(tryno=>$tryno);
7320 };7488 };
7489 if ( $EVAL_ERROR ) {
7490 PTDEBUG && _d("Try code failed:", $EVAL_ERROR);
7491 $last_error = $EVAL_ERROR;
73217492
7322 if ( defined $result ) {7493 if ( $tryno < $tries ) { # more retries
7494 my $retry = $fail->(tryno=>$tryno, error=>$last_error);
7495 last TRY unless $retry;
7496 PTDEBUG && _d("Calling wait code");
7497 $wait->(tryno=>$tryno);
7498 }
7499 }
7500 else {
7323 PTDEBUG && _d("Try code succeeded");7501 PTDEBUG && _d("Try code succeeded");
7324 if ( my $on_success = $args{on_success} ) {
7325 PTDEBUG && _d("Calling on_success code");
7326 $on_success->(tryno=>$tryno, result=>$result);
7327 }
7328 return $result;7502 return $result;
7329 }7503 }
73307504 }
7331 if ( $EVAL_ERROR ) {7505
7332 PTDEBUG && _d("Try code died:", $EVAL_ERROR);7506 PTDEBUG && _d('Try code did not succeed');
7333 die $EVAL_ERROR unless $args{retry_on_die};7507 return $final_fail->(error=>$last_error);
7334 }
7335
7336 if ( $tryno < $tries ) {
7337 PTDEBUG && _d("Try code failed, calling wait code");
7338 $wait->(tryno=>$tryno);
7339 }
7340 }
7341
7342 PTDEBUG && _d("Try code did not succeed");
7343 if ( my $on_failure = $args{on_failure} ) {
7344 PTDEBUG && _d("Calling on_failure code");
7345 $on_failure->();
7346 }
7347
7348 return;
7349}7508}
73507509
7351sub _d {7510sub _d {
@@ -8006,27 +8165,20 @@
8006 tbl => undef, # set later8165 tbl => undef, # set later
8007 };8166 };
80088167
8009 my $si = new SchemaIterator(8168 my $schema_iter = new SchemaIterator(
8010 Quoter => $args{Quoter},8169 dbh => $src->{dbh},
8170 OptionParser => $o,
8171 TableParser => $args{TableParser},
8172 Quoter => $args{Quoter},
8011 );8173 );
8012 $si->set_filter($si->make_filter($o));
80138174
8014 # Make a list of all dbs.tbls on the source. It's more efficient this8175 # Make a list of all dbs.tbls on the source. It's more efficient this
8015 # way because it avoids open/closing a dbh for each tbl and dsn, unless8176 # way because it avoids open/closing a dbh for each tbl and dsn, unless
8016 # we pre-opened the dsn. It would also cause confusing verbose output.8177 # we pre-opened the dsn. It would also cause confusing verbose output.
8017 my @dbs_tbls;8178 my @dbs_tbls;
8018 my $next_db = $si->get_db_itr(dbh => $src->{dbh});8179 while ( my $tbl = $schema_iter->next() ) {
8019 while ( my $db = $next_db->() ) {8180 PTDEBUG && _d('Got table', $tbl->{db}, $tbl->{tbl});
8020 PTDEBUG && _d('Getting tables from', $db);8181 push @dbs_tbls, $tbl;
8021 my $next_tbl = $si->get_tbl_itr(
8022 dbh => $src->{dbh},
8023 db => $db,
8024 views => 0,
8025 );
8026 while ( my $tbl = $next_tbl->() ) {
8027 PTDEBUG && _d('Got table', $tbl);
8028 push @dbs_tbls, { db => $db, tbl => $tbl };
8029 }
8030 }8182 }
80318183
8032 my $exit_status = 0;8184 my $exit_status = 0;
@@ -8048,6 +8200,7 @@
8048 lock_server(src => $src, dst => $dst, %args);8200 lock_server(src => $src, dst => $dst, %args);
80498201
8050 foreach my $db_tbl ( @dbs_tbls ) {8202 foreach my $db_tbl ( @dbs_tbls ) {
8203 $src->{tbl_struct} = $db_tbl->{tbl_struct};
8051 $src->{db} = $dst->{db} = $db_tbl->{db};8204 $src->{db} = $dst->{db} = $db_tbl->{db};
8052 $src->{tbl} = $dst->{tbl} = $db_tbl->{tbl};8205 $src->{tbl} = $dst->{tbl} = $db_tbl->{tbl};
80538206
@@ -8194,8 +8347,9 @@
8194 $start_ts = get_server_time($src->{dbh}) if $o->get('verbose');8347 $start_ts = get_server_time($src->{dbh}) if $o->get('verbose');
81958348
8196 # This will either die if there's a problem or return the tbl struct.8349 # This will either die if there's a problem or return the tbl struct.
8197 my $tbl_struct = ok_to_sync($src, $dst, %args);8350 ok_to_sync($src, $dst, %args);
8198 8351 my $tbl_struct = $src->{tbl_struct};
8352
8199 if ( my $diff = $args{diff} ) {8353 if ( my $diff = $args{diff} ) {
8200 PTDEBUG && _d('Converting checksum diff to WHERE:', Dumper($diff));8354 PTDEBUG && _d('Converting checksum diff to WHERE:', Dumper($diff));
8201 $args{where} = diff_where(8355 $args{where} = diff_where(
@@ -8568,35 +8722,30 @@
8568 }8722 }
8569 my ($src, $dst, $dp, $q, $vp, $tp, $du, $syncer, $o) = @args{@required_args};8723 my ($src, $dst, $dp, $q, $vp, $tp, $du, $syncer, $o) = @args{@required_args};
85708724
8571 # First things first: check that the src and dst dbs and tbls exist.8725 if ( !$src->{tbl_struct} ) {
8572 # This can fail in cases like h=host,D=bad,t=also_bad (i.e. simple8726 eval {
8573 # user error). It can also fail when syncing all dbs/tbls with sync_all()8727 $src->{ddl} = $tp->get_create_table(
8574 # because the dst db/tbl is assumed to be the same as the src but8728 $src->{dbh}, $src->{db}, $src->{tbl});
8575 # this isn't always the case.8729 $src->{tbl_struct} = $tp->parse($src->{ddl});
8576 my $src_tbl_ddl;8730
8577 eval {8731 };
8578 # FYI: get_create_table() does USE db but doesn't eval it.8732 if ( $EVAL_ERROR ) {
8579 $src->{dbh}->do("USE `$src->{db}`");8733 die "Error getting table structure for $src->{db}.$src->{tbl} on "
8580 $src_tbl_ddl = $du->get_create_table($src->{dbh}, $q,8734 . $dp->as_string($src->{dsn}) . "$EVAL_ERROR\nEnsure that "
8581 $src->{db}, $src->{tbl});8735 . "the table exists and is accessible.\n";
8582 };8736 }
8583 die $EVAL_ERROR if $EVAL_ERROR;8737 }
85848738
8585 my $dst_tbl_ddl;8739 # Check that the dst has the table.
8586 eval {8740 my $dst_has_table = $tp->check_table(
8587 # FYI: get_create_table() does USE db but doesn't eval it.8741 dbh => $dst->{dbh},
8588 $dst->{dbh}->do("USE `$dst->{db}`");8742 db => $dst->{db},
8589 $dst_tbl_ddl = $du->get_create_table($dst->{dbh}, $q,8743 tbl => $dst->{tbl},
8590 $dst->{db}, $dst->{tbl});8744 );
8591 };8745 if ( !$dst_has_table ) {
8592 die $EVAL_ERROR if $EVAL_ERROR;8746 die "Table $dst->{db}.$dst->{tbl} does not exist on "
85938747 . $dp->as_string($dst->{dsn}) . "\n";
8594 # This doesn't work at the moment when syncing different table names.8748 }
8595 # Check that src.db.tbl has the exact same schema as dst.db.tbl.
8596 # if ( $o->get('check-schema') && ($src_tbl_ddl ne $dst_tbl_ddl) ) {
8597 # die "Source and destination tables have different schemas";
8598 # }
8599 my $tbl_struct = $tp->parse($src_tbl_ddl);
86008749
8601 # Check that the user has all the necessary privs on the tbls.8750 # Check that the user has all the necessary privs on the tbls.
8602 if ( $o->get('check-privileges') ) {8751 if ( $o->get('check-privileges') ) {
@@ -8629,7 +8778,7 @@
8629 }8778 }
8630 }8779 }
86318780
8632 return $tbl_struct;8781 return;
8633}8782}
86348783
8635# Sub: filter_diffs8784# Sub: filter_diffs
86368785
=== added file 'bin/pt-table-usage'
--- bin/pt-table-usage 1970-01-01 00:00:00 +0000
+++ bin/pt-table-usage 2012-03-31 16:07:24 +0000
@@ -0,0 +1,7320 @@
1#!/usr/bin/env perl
2
3# This program is part of Percona Toolkit: http://www.percona.com/software/
4# See "COPYRIGHT, LICENSE, AND WARRANTY" at the end of this file for legal
5# notices and disclaimers.
6
7use strict;
8use warnings FATAL => 'all';
9use constant PTDEBUG => $ENV{PTDEBUG} || 0;
10
11# ###########################################################################
12# DSNParser package
13# This package is a copy without comments from the original. The original
14# with comments and its test file can be found in the Bazaar repository at,
15# lib/DSNParser.pm
16# t/lib/DSNParser.t
17# See https://launchpad.net/percona-toolkit for more information.
18# ###########################################################################
19{
20package DSNParser;
21
22use strict;
23use warnings FATAL => 'all';
24use English qw(-no_match_vars);
25use constant PTDEBUG => $ENV{PTDEBUG} || 0;
26
27use Data::Dumper;
28$Data::Dumper::Indent = 0;
29$Data::Dumper::Quotekeys = 0;
30
31eval {
32 require DBI;
33};
34my $have_dbi = $EVAL_ERROR ? 0 : 1;
35
36sub new {
37 my ( $class, %args ) = @_;
38 foreach my $arg ( qw(opts) ) {
39 die "I need a $arg argument" unless $args{$arg};
40 }
41 my $self = {
42 opts => {} # h, P, u, etc. Should come from DSN OPTIONS section in POD.
43 };
44 foreach my $opt ( @{$args{opts}} ) {
45 if ( !$opt->{key} || !$opt->{desc} ) {
46 die "Invalid DSN option: ", Dumper($opt);
47 }
48 PTDEBUG && _d('DSN option:',
49 join(', ',
50 map { "$_=" . (defined $opt->{$_} ? ($opt->{$_} || '') : 'undef') }
51 keys %$opt
52 )
53 );
54 $self->{opts}->{$opt->{key}} = {
55 dsn => $opt->{dsn},
56 desc => $opt->{desc},
57 copy => $opt->{copy} || 0,
58 };
59 }
60 return bless $self, $class;
61}
62
63sub prop {
64 my ( $self, $prop, $value ) = @_;
65 if ( @_ > 2 ) {
66 PTDEBUG && _d('Setting', $prop, 'property');
67 $self->{$prop} = $value;
68 }
69 return $self->{$prop};
70}
71
72sub parse {
73 my ( $self, $dsn, $prev, $defaults ) = @_;
74 if ( !$dsn ) {
75 PTDEBUG && _d('No DSN to parse');
76 return;
77 }
78 PTDEBUG && _d('Parsing', $dsn);
79 $prev ||= {};
80 $defaults ||= {};
81 my %given_props;
82 my %final_props;
83 my $opts = $self->{opts};
84
85 foreach my $dsn_part ( split(/,/, $dsn) ) {
86 if ( my ($prop_key, $prop_val) = $dsn_part =~ m/^(.)=(.*)$/ ) {
87 $given_props{$prop_key} = $prop_val;
88 }
89 else {
90 PTDEBUG && _d('Interpreting', $dsn_part, 'as h=', $dsn_part);
91 $given_props{h} = $dsn_part;
92 }
93 }
94
95 foreach my $key ( keys %$opts ) {
96 PTDEBUG && _d('Finding value for', $key);
97 $final_props{$key} = $given_props{$key};
98 if ( !defined $final_props{$key}
99 && defined $prev->{$key} && $opts->{$key}->{copy} )
100 {
101 $final_props{$key} = $prev->{$key};
102 PTDEBUG && _d('Copying value for', $key, 'from previous DSN');
103 }
104 if ( !defined $final_props{$key} ) {
105 $final_props{$key} = $defaults->{$key};
106 PTDEBUG && _d('Copying value for', $key, 'from defaults');
107 }
108 }
109
110 foreach my $key ( keys %given_props ) {
111 die "Unknown DSN option '$key' in '$dsn'. For more details, "
112 . "please use the --help option, or try 'perldoc $PROGRAM_NAME' "
113 . "for complete documentation."
114 unless exists $opts->{$key};
115 }
116 if ( (my $required = $self->prop('required')) ) {
117 foreach my $key ( keys %$required ) {
118 die "Missing required DSN option '$key' in '$dsn'. For more details, "
119 . "please use the --help option, or try 'perldoc $PROGRAM_NAME' "
120 . "for complete documentation."
121 unless $final_props{$key};
122 }
123 }
124
125 return \%final_props;
126}
127
128sub parse_options {
129 my ( $self, $o ) = @_;
130 die 'I need an OptionParser object' unless ref $o eq 'OptionParser';
131 my $dsn_string
132 = join(',',
133 map { "$_=".$o->get($_); }
134 grep { $o->has($_) && $o->get($_) }
135 keys %{$self->{opts}}
136 );
137 PTDEBUG && _d('DSN string made from options:', $dsn_string);
138 return $self->parse($dsn_string);
139}
140
141sub as_string {
142 my ( $self, $dsn, $props ) = @_;
143 return $dsn unless ref $dsn;
144 my @keys = $props ? @$props : sort keys %$dsn;
145 return join(',',
146 map { "$_=" . ($_ eq 'p' ? '...' : $dsn->{$_}) }
147 grep {
148 exists $self->{opts}->{$_}
149 && exists $dsn->{$_}
150 && defined $dsn->{$_}
151 } @keys);
152}
153
154sub usage {
155 my ( $self ) = @_;
156 my $usage
157 = "DSN syntax is key=value[,key=value...] Allowable DSN keys:\n\n"
158 . " KEY COPY MEANING\n"
159 . " === ==== =============================================\n";
160 my %opts = %{$self->{opts}};
161 foreach my $key ( sort keys %opts ) {
162 $usage .= " $key "
163 . ($opts{$key}->{copy} ? 'yes ' : 'no ')
164 . ($opts{$key}->{desc} || '[No description]')
165 . "\n";
166 }
167 $usage .= "\n If the DSN is a bareword, the word is treated as the 'h' key.\n";
168 return $usage;
169}
170
171sub get_cxn_params {
172 my ( $self, $info ) = @_;
173 my $dsn;
174 my %opts = %{$self->{opts}};
175 my $driver = $self->prop('dbidriver') || '';
176 if ( $driver eq 'Pg' ) {
177 $dsn = 'DBI:Pg:dbname=' . ( $info->{D} || '' ) . ';'
178 . join(';', map { "$opts{$_}->{dsn}=$info->{$_}" }
179 grep { defined $info->{$_} }
180 qw(h P));
181 }
182 else {
183 $dsn = 'DBI:mysql:' . ( $info->{D} || '' ) . ';'
184 . join(';', map { "$opts{$_}->{dsn}=$info->{$_}" }
185 grep { defined $info->{$_} }
186 qw(F h P S A))
187 . ';mysql_read_default_group=client';
188 }
189 PTDEBUG && _d($dsn);
190 return ($dsn, $info->{u}, $info->{p});
191}
192
193sub fill_in_dsn {
194 my ( $self, $dbh, $dsn ) = @_;
195 my $vars = $dbh->selectall_hashref('SHOW VARIABLES', 'Variable_name');
196 my ($user, $db) = $dbh->selectrow_array('SELECT USER(), DATABASE()');
197 $user =~ s/@.*//;
198 $dsn->{h} ||= $vars->{hostname}->{Value};
199 $dsn->{S} ||= $vars->{'socket'}->{Value};
200 $dsn->{P} ||= $vars->{port}->{Value};
201 $dsn->{u} ||= $user;
202 $dsn->{D} ||= $db;
203}
204
205sub get_dbh {
206 my ( $self, $cxn_string, $user, $pass, $opts ) = @_;
207 $opts ||= {};
208 my $defaults = {
209 AutoCommit => 0,
210 RaiseError => 1,
211 PrintError => 0,
212 ShowErrorStatement => 1,
213 mysql_enable_utf8 => ($cxn_string =~ m/charset=utf8/i ? 1 : 0),
214 };
215 @{$defaults}{ keys %$opts } = values %$opts;
216
217 if ( $opts->{mysql_use_result} ) {
218 $defaults->{mysql_use_result} = 1;
219 }
220
221 if ( !$have_dbi ) {
222 die "Cannot connect to MySQL because the Perl DBI module is not "
223 . "installed or not found. Run 'perl -MDBI' to see the directories "
224 . "that Perl searches for DBI. If DBI is not installed, try:\n"
225 . " Debian/Ubuntu apt-get install libdbi-perl\n"
226 . " RHEL/CentOS yum install perl-DBI\n"
227 . " OpenSolaris pgk install pkg:/SUNWpmdbi\n";
228
229 }
230
231 my $dbh;
232 my $tries = 2;
233 while ( !$dbh && $tries-- ) {
234 PTDEBUG && _d($cxn_string, ' ', $user, ' ', $pass,
235 join(', ', map { "$_=>$defaults->{$_}" } keys %$defaults ));
236
237 eval {
238 $dbh = DBI->connect($cxn_string, $user, $pass, $defaults);
239
240 if ( $cxn_string =~ m/mysql/i ) {
241 my $sql;
242
243 $sql = 'SELECT @@SQL_MODE';
244 PTDEBUG && _d($dbh, $sql);
245 my ($sql_mode) = $dbh->selectrow_array($sql);
246
247 $sql = 'SET @@SQL_QUOTE_SHOW_CREATE = 1'
248 . '/*!40101, @@SQL_MODE=\'NO_AUTO_VALUE_ON_ZERO'
249 . ($sql_mode ? ",$sql_mode" : '')
250 . '\'*/';
251 PTDEBUG && _d($dbh, $sql);
252 $dbh->do($sql);
253
254 if ( my ($charset) = $cxn_string =~ m/charset=(\w+)/ ) {
255 $sql = "/*!40101 SET NAMES $charset*/";
256 PTDEBUG && _d($dbh, ':', $sql);
257 $dbh->do($sql);
258 PTDEBUG && _d('Enabling charset for STDOUT');
259 if ( $charset eq 'utf8' ) {
260 binmode(STDOUT, ':utf8')
261 or die "Can't binmode(STDOUT, ':utf8'): $OS_ERROR";
262 }
263 else {
264 binmode(STDOUT) or die "Can't binmode(STDOUT): $OS_ERROR";
265 }
266 }
267
268 if ( $self->prop('set-vars') ) {
269 $sql = "SET " . $self->prop('set-vars');
270 PTDEBUG && _d($dbh, ':', $sql);
271 $dbh->do($sql);
272 }
273 }
274 };
275 if ( !$dbh && $EVAL_ERROR ) {
276 PTDEBUG && _d($EVAL_ERROR);
277 if ( $EVAL_ERROR =~ m/not a compiled character set|character set utf8/ ) {
278 PTDEBUG && _d('Going to try again without utf8 support');
279 delete $defaults->{mysql_enable_utf8};
280 }
281 elsif ( $EVAL_ERROR =~ m/locate DBD\/mysql/i ) {
282 die "Cannot connect to MySQL because the Perl DBD::mysql module is "
283 . "not installed or not found. Run 'perl -MDBD::mysql' to see "
284 . "the directories that Perl searches for DBD::mysql. If "
285 . "DBD::mysql is not installed, try:\n"
286 . " Debian/Ubuntu apt-get install libdbd-mysql-perl\n"
287 . " RHEL/CentOS yum install perl-DBD-MySQL\n"
288 . " OpenSolaris pgk install pkg:/SUNWapu13dbd-mysql\n";
289 }
290 if ( !$tries ) {
291 die $EVAL_ERROR;
292 }
293 }
294 }
295
296 PTDEBUG && _d('DBH info: ',
297 $dbh,
298 Dumper($dbh->selectrow_hashref(
299 'SELECT DATABASE(), CONNECTION_ID(), VERSION()/*!50038 , @@hostname*/')),
300 'Connection info:', $dbh->{mysql_hostinfo},
301 'Character set info:', Dumper($dbh->selectall_arrayref(
302 'SHOW VARIABLES LIKE "character_set%"', { Slice => {}})),
303 '$DBD::mysql::VERSION:', $DBD::mysql::VERSION,
304 '$DBI::VERSION:', $DBI::VERSION,
305 );
306
307 return $dbh;
308}
309
310sub get_hostname {
311 my ( $self, $dbh ) = @_;
312 if ( my ($host) = ($dbh->{mysql_hostinfo} || '') =~ m/^(\w+) via/ ) {
313 return $host;
314 }
315 my ( $hostname, $one ) = $dbh->selectrow_array(
316 'SELECT /*!50038 @@hostname, */ 1');
317 return $hostname;
318}
319
320sub disconnect {
321 my ( $self, $dbh ) = @_;
322 PTDEBUG && $self->print_active_handles($dbh);
323 $dbh->disconnect;
324}
325
326sub print_active_handles {
327 my ( $self, $thing, $level ) = @_;
328 $level ||= 0;
329 printf("# Active %sh: %s %s %s\n", ($thing->{Type} || 'undef'), "\t" x $level,
330 $thing, (($thing->{Type} || '') eq 'st' ? $thing->{Statement} || '' : ''))
331 or die "Cannot print: $OS_ERROR";
332 foreach my $handle ( grep {defined} @{ $thing->{ChildHandles} } ) {
333 $self->print_active_handles( $handle, $level + 1 );
334 }
335}
336
337sub copy {
338 my ( $self, $dsn_1, $dsn_2, %args ) = @_;
339 die 'I need a dsn_1 argument' unless $dsn_1;
340 die 'I need a dsn_2 argument' unless $dsn_2;
341 my %new_dsn = map {
342 my $key = $_;
343 my $val;
344 if ( $args{overwrite} ) {
345 $val = defined $dsn_1->{$key} ? $dsn_1->{$key} : $dsn_2->{$key};
346 }
347 else {
348 $val = defined $dsn_2->{$key} ? $dsn_2->{$key} : $dsn_1->{$key};
349 }
350 $key => $val;
351 } keys %{$self->{opts}};
352 return \%new_dsn;
353}
354
355sub _d {
356 my ($package, undef, $line) = caller 0;
357 @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; }
358 map { defined $_ ? $_ : 'undef' }
359 @_;
360 print STDERR "# $package:$line $PID ", join(' ', @_), "\n";
361}
362
3631;
364}
365# ###########################################################################
366# End DSNParser package
367# ###########################################################################
368
369# ###########################################################################
370# OptionParser package
371# This package is a copy without comments from the original. The original
372# with comments and its test file can be found in the Bazaar repository at,
373# lib/OptionParser.pm
374# t/lib/OptionParser.t
375# See https://launchpad.net/percona-toolkit for more information.
376# ###########################################################################
377{
378package OptionParser;
379
380use strict;
381use warnings FATAL => 'all';
382use English qw(-no_match_vars);
383use constant PTDEBUG => $ENV{PTDEBUG} || 0;
384
385use List::Util qw(max);
386use Getopt::Long;
387
388my $POD_link_re = '[LC]<"?([^">]+)"?>';
389
390sub new {
391 my ( $class, %args ) = @_;
392 my @required_args = qw();
393 foreach my $arg ( @required_args ) {
394 die "I need a $arg argument" unless $args{$arg};
395 }
396
397 my ($program_name) = $PROGRAM_NAME =~ m/([.A-Za-z-]+)$/;
398 $program_name ||= $PROGRAM_NAME;
399 my $home = $ENV{HOME} || $ENV{HOMEPATH} || $ENV{USERPROFILE} || '.';
400
401 my %attributes = (
402 'type' => 1,
403 'short form' => 1,
404 'group' => 1,
405 'default' => 1,
406 'cumulative' => 1,
407 'negatable' => 1,
408 );
409
410 my $self = {
411 head1 => 'OPTIONS', # These args are used internally
412 skip_rules => 0, # to instantiate another Option-
413 item => '--(.*)', # Parser obj that parses the
414 attributes => \%attributes, # DSN OPTIONS section. Tools
415 parse_attributes => \&_parse_attribs, # don't tinker with these args.
416
417 %args,
418
419 strict => 1, # disabled by a special rule
420 program_name => $program_name,
421 opts => {},
422 got_opts => 0,
423 short_opts => {},
424 defaults => {},
425 groups => {},
426 allowed_groups => {},
427 errors => [],
428 rules => [], # desc of rules for --help
429 mutex => [], # rule: opts are mutually exclusive
430 atleast1 => [], # rule: at least one opt is required
431 disables => {}, # rule: opt disables other opts
432 defaults_to => {}, # rule: opt defaults to value of other opt
433 DSNParser => undef,
434 default_files => [
435 "/etc/percona-toolkit/percona-toolkit.conf",
436 "/etc/percona-toolkit/$program_name.conf",
437 "$home/.percona-toolkit.conf",
438 "$home/.$program_name.conf",
439 ],
440 types => {
441 string => 's', # standard Getopt type
442 int => 'i', # standard Getopt type
443 float => 'f', # standard Getopt type
444 Hash => 'H', # hash, formed from a comma-separated list
445 hash => 'h', # hash as above, but only if a value is given
446 Array => 'A', # array, similar to Hash
447 array => 'a', # array, similar to hash
448 DSN => 'd', # DSN
449 size => 'z', # size with kMG suffix (powers of 2^10)
450 time => 'm', # time, with an optional suffix of s/h/m/d
451 },
452 };
453
454 return bless $self, $class;
455}
456
457sub get_specs {
458 my ( $self, $file ) = @_;
459 $file ||= $self->{file} || __FILE__;
460 my @specs = $self->_pod_to_specs($file);
461 $self->_parse_specs(@specs);
462
463 open my $fh, "<", $file or die "Cannot open $file: $OS_ERROR";
464 my $contents = do { local $/ = undef; <$fh> };
465 close $fh;
466 if ( $contents =~ m/^=head1 DSN OPTIONS/m ) {
467 PTDEBUG && _d('Parsing DSN OPTIONS');
468 my $dsn_attribs = {
469 dsn => 1,
470 copy => 1,
471 };
472 my $parse_dsn_attribs = sub {
473 my ( $self, $option, $attribs ) = @_;
474 map {
475 my $val = $attribs->{$_};
476 if ( $val ) {
477 $val = $val eq 'yes' ? 1
478 : $val eq 'no' ? 0
479 : $val;
480 $attribs->{$_} = $val;
481 }
482 } keys %$attribs;
483 return {
484 key => $option,
485 %$attribs,
486 };
487 };
488 my $dsn_o = new OptionParser(
489 description => 'DSN OPTIONS',
490 head1 => 'DSN OPTIONS',
491 dsn => 0, # XXX don't infinitely recurse!
492 item => '\* (.)', # key opts are a single character
493 skip_rules => 1, # no rules before opts
494 attributes => $dsn_attribs,
495 parse_attributes => $parse_dsn_attribs,
496 );
497 my @dsn_opts = map {
498 my $opts = {
499 key => $_->{spec}->{key},
500 dsn => $_->{spec}->{dsn},
501 copy => $_->{spec}->{copy},
502 desc => $_->{desc},
503 };
504 $opts;
505 } $dsn_o->_pod_to_specs($file);
506 $self->{DSNParser} = DSNParser->new(opts => \@dsn_opts);
507 }
508
509 if ( $contents =~ m/^=head1 VERSION\n\n^(.+)$/m ) {
510 $self->{version} = $1;
511 PTDEBUG && _d($self->{version});
512 }
513
514 return;
515}
516
517sub DSNParser {
518 my ( $self ) = @_;
519 return $self->{DSNParser};
520};
521
522sub get_defaults_files {
523 my ( $self ) = @_;
524 return @{$self->{default_files}};
525}
526
527sub _pod_to_specs {
528 my ( $self, $file ) = @_;
529 $file ||= $self->{file} || __FILE__;
530 open my $fh, '<', $file or die "Cannot open $file: $OS_ERROR";
531
532 my @specs = ();
533 my @rules = ();
534 my $para;
535
536 local $INPUT_RECORD_SEPARATOR = '';
537 while ( $para = <$fh> ) {
538 next unless $para =~ m/^=head1 $self->{head1}/;
539 last;
540 }
541
542 while ( $para = <$fh> ) {
543 last if $para =~ m/^=over/;
544 next if $self->{skip_rules};
545 chomp $para;
546 $para =~ s/\s+/ /g;
547 $para =~ s/$POD_link_re/$1/go;
548 PTDEBUG && _d('Option rule:', $para);
549 push @rules, $para;
550 }
551
552 die "POD has no $self->{head1} section" unless $para;
553
554 do {
555 if ( my ($option) = $para =~ m/^=item $self->{item}/ ) {
556 chomp $para;
557 PTDEBUG && _d($para);
558 my %attribs;
559
560 $para = <$fh>; # read next paragraph, possibly attributes
561
562 if ( $para =~ m/: / ) { # attributes
563 $para =~ s/\s+\Z//g;
564 %attribs = map {
565 my ( $attrib, $val) = split(/: /, $_);
566 die "Unrecognized attribute for --$option: $attrib"
567 unless $self->{attributes}->{$attrib};
568 ($attrib, $val);
569 } split(/; /, $para);
570 if ( $attribs{'short form'} ) {
571 $attribs{'short form'} =~ s/-//;
572 }
573 $para = <$fh>; # read next paragraph, probably short help desc
574 }
575 else {
576 PTDEBUG && _d('Option has no attributes');
577 }
578
579 $para =~ s/\s+\Z//g;
580 $para =~ s/\s+/ /g;
581 $para =~ s/$POD_link_re/$1/go;
582
583 $para =~ s/\.(?:\n.*| [A-Z].*|\Z)//s;
584 PTDEBUG && _d('Short help:', $para);
585
586 die "No description after option spec $option" if $para =~ m/^=item/;
587
588 if ( my ($base_option) = $option =~ m/^\[no\](.*)/ ) {
589 $option = $base_option;
590 $attribs{'negatable'} = 1;
591 }
592
593 push @specs, {
594 spec => $self->{parse_attributes}->($self, $option, \%attribs),
595 desc => $para
596 . (defined $attribs{default} ? " (default $attribs{default})" : ''),
597 group => ($attribs{'group'} ? $attribs{'group'} : 'default'),
598 };
599 }
600 while ( $para = <$fh> ) {
601 last unless $para;
602 if ( $para =~ m/^=head1/ ) {
603 $para = undef; # Can't 'last' out of a do {} block.
604 last;
605 }
606 last if $para =~ m/^=item /;
607 }
608 } while ( $para );
609
610 die "No valid specs in $self->{head1}" unless @specs;
611
612 close $fh;
613 return @specs, @rules;
614}
615
616sub _parse_specs {
617 my ( $self, @specs ) = @_;
618 my %disables; # special rule that requires deferred checking
619
620 foreach my $opt ( @specs ) {
621 if ( ref $opt ) { # It's an option spec, not a rule.
622 PTDEBUG && _d('Parsing opt spec:',
623 map { ($_, '=>', $opt->{$_}) } keys %$opt);
624
625 my ( $long, $short ) = $opt->{spec} =~ m/^([\w-]+)(?:\|([^!+=]*))?/;
626 if ( !$long ) {
627 die "Cannot parse long option from spec $opt->{spec}";
628 }
629 $opt->{long} = $long;
630
631 die "Duplicate long option --$long" if exists $self->{opts}->{$long};
632 $self->{opts}->{$long} = $opt;
633
634 if ( length $long == 1 ) {
635 PTDEBUG && _d('Long opt', $long, 'looks like short opt');
636 $self->{short_opts}->{$long} = $long;
637 }
638
639 if ( $short ) {
640 die "Duplicate short option -$short"
641 if exists $self->{short_opts}->{$short};
642 $self->{short_opts}->{$short} = $long;
643 $opt->{short} = $short;
644 }
645 else {
646 $opt->{short} = undef;
647 }
648
649 $opt->{is_negatable} = $opt->{spec} =~ m/!/ ? 1 : 0;
650 $opt->{is_cumulative} = $opt->{spec} =~ m/\+/ ? 1 : 0;
651 $opt->{is_required} = $opt->{desc} =~ m/required/ ? 1 : 0;
652
653 $opt->{group} ||= 'default';
654 $self->{groups}->{ $opt->{group} }->{$long} = 1;
655
656 $opt->{value} = undef;
657 $opt->{got} = 0;
658
659 my ( $type ) = $opt->{spec} =~ m/=(.)/;
660 $opt->{type} = $type;
661 PTDEBUG && _d($long, 'type:', $type);
662
663
664 $opt->{spec} =~ s/=./=s/ if ( $type && $type =~ m/[HhAadzm]/ );
665
666 if ( (my ($def) = $opt->{desc} =~ m/default\b(?: ([^)]+))?/) ) {
667 $self->{defaults}->{$long} = defined $def ? $def : 1;
668 PTDEBUG && _d($long, 'default:', $def);
669 }
670
671 if ( $long eq 'config' ) {
672 $self->{defaults}->{$long} = join(',', $self->get_defaults_files());
673 }
674
675 if ( (my ($dis) = $opt->{desc} =~ m/(disables .*)/) ) {
676 $disables{$long} = $dis;
677 PTDEBUG && _d('Deferring check of disables rule for', $opt, $dis);
678 }
679
680 $self->{opts}->{$long} = $opt;
681 }
682 else { # It's an option rule, not a spec.
683 PTDEBUG && _d('Parsing rule:', $opt);
684 push @{$self->{rules}}, $opt;
685 my @participants = $self->_get_participants($opt);
686 my $rule_ok = 0;
687
688 if ( $opt =~ m/mutually exclusive|one and only one/ ) {
689 $rule_ok = 1;
690 push @{$self->{mutex}}, \@participants;
691 PTDEBUG && _d(@participants, 'are mutually exclusive');
692 }
693 if ( $opt =~ m/at least one|one and only one/ ) {
694 $rule_ok = 1;
695 push @{$self->{atleast1}}, \@participants;
696 PTDEBUG && _d(@participants, 'require at least one');
697 }
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches