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
1=== modified file 'bin/pt-duplicate-key-checker'
2--- bin/pt-duplicate-key-checker 2012-03-07 23:41:54 +0000
3+++ bin/pt-duplicate-key-checker 2012-03-31 16:07:24 +0000
4@@ -199,19 +199,58 @@
5 return bless $self, $class;
6 }
7
8+sub get_create_table {
9+ my ( $self, $dbh, $db, $tbl ) = @_;
10+ die "I need a dbh parameter" unless $dbh;
11+ die "I need a db parameter" unless $db;
12+ die "I need a tbl parameter" unless $tbl;
13+ my $q = $self->{Quoter};
14+
15+ my $new_sql_mode
16+ = '/*!40101 SET @OLD_SQL_MODE := @@SQL_MODE, '
17+ . q{@@SQL_MODE := REPLACE(REPLACE(@@SQL_MODE, 'ANSI_QUOTES', ''), ',,', ','), }
18+ . '@OLD_QUOTE := @@SQL_QUOTE_SHOW_CREATE, '
19+ . '@@SQL_QUOTE_SHOW_CREATE := 1 */';
20+
21+ my $old_sql_mode = '/*!40101 SET @@SQL_MODE := @OLD_SQL_MODE, '
22+ . '@@SQL_QUOTE_SHOW_CREATE := @OLD_QUOTE */';
23+
24+ PTDEBUG && _d($new_sql_mode);
25+ eval { $dbh->do($new_sql_mode); };
26+ PTDEBUG && $EVAL_ERROR && _d($EVAL_ERROR);
27+
28+ my $use_sql = 'USE ' . $q->quote($db);
29+ PTDEBUG && _d($dbh, $use_sql);
30+ $dbh->do($use_sql);
31+
32+ my $show_sql = "SHOW CREATE TABLE " . $q->quote($db, $tbl);
33+ PTDEBUG && _d($show_sql);
34+ my $href;
35+ eval { $href = $dbh->selectrow_hashref($show_sql); };
36+ if ( $EVAL_ERROR ) {
37+ PTDEBUG && _d($EVAL_ERROR);
38+
39+ PTDEBUG && _d($old_sql_mode);
40+ $dbh->do($old_sql_mode);
41+
42+ return;
43+ }
44+
45+ PTDEBUG && _d($old_sql_mode);
46+ $dbh->do($old_sql_mode);
47+
48+ my ($key) = grep { m/create (?:table|view)/i } keys %$href;
49+ if ( !$key ) {
50+ die "Error: no 'Create Table' or 'Create View' in result set from "
51+ . "$show_sql: " . Dumper($href);
52+ }
53+
54+ return $href->{$key};
55+}
56+
57 sub parse {
58 my ( $self, $ddl, $opts ) = @_;
59 return unless $ddl;
60- if ( ref $ddl eq 'ARRAY' ) {
61- if ( lc $ddl->[0] eq 'table' ) {
62- $ddl = $ddl->[1];
63- }
64- else {
65- return {
66- engine => 'VIEW',
67- };
68- }
69- }
70
71 if ( $ddl !~ m/CREATE (?:TEMPORARY )?TABLE `/ ) {
72 die "Cannot parse table definition; is ANSI quoting "
73@@ -518,41 +557,31 @@
74 return $ddl;
75 }
76
77-sub remove_secondary_indexes {
78- my ( $self, $ddl ) = @_;
79- my $sec_indexes_ddl;
80- my $tbl_struct = $self->parse($ddl);
81-
82- if ( ($tbl_struct->{engine} || '') =~ m/InnoDB/i ) {
83- my $clustered_key = $tbl_struct->{clustered_key};
84- $clustered_key ||= '';
85-
86- my @sec_indexes = map {
87- my $key_def = $_->{ddl};
88- $key_def =~ s/([\(\)])/\\$1/g;
89- $ddl =~ s/\s+$key_def//i;
90-
91- my $key_ddl = "ADD $_->{ddl}";
92- $key_ddl .= ',' unless $key_ddl =~ m/,$/;
93- $key_ddl;
94- }
95- grep { $_->{name} ne $clustered_key }
96- values %{$tbl_struct->{keys}};
97- PTDEBUG && _d('Secondary indexes:', Dumper(\@sec_indexes));
98-
99- if ( @sec_indexes ) {
100- $sec_indexes_ddl = join(' ', @sec_indexes);
101- $sec_indexes_ddl =~ s/,$//;
102- }
103-
104- $ddl =~ s/,(\n\) )/$1/s;
105- }
106- else {
107- PTDEBUG && _d('Not removing secondary indexes from',
108- $tbl_struct->{engine}, 'table');
109- }
110-
111- return $ddl, $sec_indexes_ddl, $tbl_struct;
112+sub get_table_status {
113+ my ( $self, $dbh, $db, $like ) = @_;
114+ my $q = $self->{Quoter};
115+ my $sql = "SHOW TABLE STATUS FROM " . $q->quote($db);
116+ my @params;
117+ if ( $like ) {
118+ $sql .= ' LIKE ?';
119+ push @params, $like;
120+ }
121+ PTDEBUG && _d($sql, @params);
122+ my $sth = $dbh->prepare($sql);
123+ eval { $sth->execute(@params); };
124+ if ($EVAL_ERROR) {
125+ PTDEBUG && _d($EVAL_ERROR);
126+ return;
127+ }
128+ my @tables = @{$sth->fetchall_arrayref({})};
129+ @tables = map {
130+ my %tbl; # Make a copy with lowercased keys
131+ @tbl{ map { lc $_ } keys %$_ } = values %$_;
132+ $tbl{engine} ||= $tbl{type} || $tbl{comment};
133+ delete $tbl{type};
134+ \%tbl;
135+ } @tables;
136+ return @tables;
137 }
138
139 sub _d {
140@@ -3195,7 +3224,7 @@
141
142 sub new {
143 my ( $class, %args ) = @_;
144- my @required_args = qw(OptionParser Quoter);
145+ my @required_args = qw(OptionParser TableParser Quoter);
146 foreach my $arg ( @required_args ) {
147 die "I need a $arg argument" unless $args{$arg};
148 }
149@@ -3204,8 +3233,19 @@
150 die "I need either a dbh or file_itr argument"
151 if (!$dbh && !$file_itr) || ($dbh && $file_itr);
152
153+ my %resume;
154+ if ( my $table = $args{resume} ) {
155+ PTDEBUG && _d('Will resume from or after', $table);
156+ my ($db, $tbl) = $args{Quoter}->split_unquote($table);
157+ die "Resume table must be database-qualified: $table"
158+ unless $db && $tbl;
159+ $resume{db} = $db;
160+ $resume{tbl} = $tbl;
161+ }
162+
163 my $self = {
164 %args,
165+ resume => \%resume,
166 filters => _make_filters(%args),
167 };
168
169@@ -3266,9 +3306,19 @@
170 return \%filters;
171 }
172
173-sub next_schema_object {
174+sub next {
175 my ( $self ) = @_;
176
177+ if ( !$self->{initialized} ) {
178+ $self->{initialized} = 1;
179+ if ( $self->{resume}->{tbl}
180+ && !$self->table_is_allowed(@{$self->{resume}}{qw(db tbl)}) ) {
181+ PTDEBUG && _d('Will resume after',
182+ join('.', @{$self->{resume}}{qw(db tbl)}));
183+ $self->{resume}->{after} = 1;
184+ }
185+ }
186+
187 my $schema_obj;
188 if ( $self->{file_itr} ) {
189 $schema_obj= $self->_iterate_files();
190@@ -3278,24 +3328,18 @@
191 }
192
193 if ( $schema_obj ) {
194- if ( $schema_obj->{ddl} && $self->{TableParser} ) {
195- $schema_obj->{tbl_struct}
196- = $self->{TableParser}->parse($schema_obj->{ddl});
197- }
198-
199- delete $schema_obj->{ddl} unless $self->{keep_ddl};
200-
201 if ( my $schema = $self->{Schema} ) {
202 $schema->add_schema_object($schema_obj);
203 }
204+ PTDEBUG && _d('Next schema object:',
205+ $schema_obj->{db}, $schema_obj->{tbl});
206 }
207
208- PTDEBUG && _d('Next schema object:', $schema_obj->{db}, $schema_obj->{tbl});
209 return $schema_obj;
210 }
211
212 sub _iterate_files {
213- my ( $self ) = @_;
214+ my ( $self ) = @_;
215
216 if ( !$self->{fh} ) {
217 my ($fh, $file) = $self->{file_itr}->();
218@@ -3316,7 +3360,8 @@
219 my $db = $1; # XXX
220 $db =~ s/^`//; # strip leading `
221 $db =~ s/`$//; # and trailing `
222- if ( $self->database_is_allowed($db) ) {
223+ if ( $self->database_is_allowed($db)
224+ && $self->_resume_from_database($db) ) {
225 $self->{db} = $db;
226 }
227 }
228@@ -3329,21 +3374,22 @@
229 my ($tbl) = $chunk =~ m/$tbl_name/;
230 $tbl =~ s/^\s*`//;
231 $tbl =~ s/`\s*$//;
232- if ( $self->table_is_allowed($self->{db}, $tbl) ) {
233+ if ( $self->_resume_from_table($tbl)
234+ && $self->table_is_allowed($self->{db}, $tbl) ) {
235 my ($ddl) = $chunk =~ m/^(?:$open_comment)?(CREATE TABLE.+?;)$/ms;
236 if ( !$ddl ) {
237 warn "Failed to parse CREATE TABLE from\n" . $chunk;
238 next CHUNK;
239 }
240 $ddl =~ s/ \*\/;\Z/;/; # remove end of version comment
241-
242- my ($engine) = $ddl =~ m/\).*?(?:ENGINE|TYPE)=(\w+)/;
243-
244- if ( !$engine || $self->engine_is_allowed($engine) ) {
245+ my $tbl_struct = $self->{TableParser}->parse($ddl);
246+ if ( $self->engine_is_allowed($tbl_struct->{engine}) ) {
247 return {
248- db => $self->{db},
249- tbl => $tbl,
250- ddl => $ddl,
251+ db => $self->{db},
252+ tbl => $tbl,
253+ name => $self->{Quoter}->quote($self->{db}, $tbl),
254+ ddl => $ddl,
255+ tbl_struct => $tbl_struct,
256 };
257 }
258 }
259@@ -3360,6 +3406,7 @@
260 sub _iterate_dbh {
261 my ( $self ) = @_;
262 my $q = $self->{Quoter};
263+ my $tp = $self->{TableParser};
264 my $dbh = $self->{dbh};
265 PTDEBUG && _d('Getting next schema object from dbh', $dbh);
266
267@@ -3373,7 +3420,9 @@
268 }
269
270 if ( !$self->{db} ) {
271- $self->{db} = shift @{$self->{dbs}};
272+ do {
273+ $self->{db} = shift @{$self->{dbs}};
274+ } until $self->_resume_from_database($self->{db});
275 PTDEBUG && _d('Next database:', $self->{db});
276 return unless $self->{db};
277 }
278@@ -3386,8 +3435,9 @@
279 }
280 grep {
281 my ($tbl, $type) = @$_;
282- $self->table_is_allowed($self->{db}, $tbl)
283- && (!$type || ($type ne 'VIEW'));
284+ (!$type || ($type ne 'VIEW'))
285+ && $self->_resume_from_table($tbl)
286+ && $self->table_is_allowed($self->{db}, $tbl);
287 }
288 @{$dbh->selectall_arrayref($sql)};
289 PTDEBUG && _d('Found', scalar @tbls, 'tables in database', $self->{db});
290@@ -3395,27 +3445,15 @@
291 }
292
293 while ( my $tbl = shift @{$self->{tbls}} ) {
294- my $engine;
295- if ( $self->{filters}->{'engines'}
296- || $self->{filters}->{'ignore-engines'} ) {
297- my $sql = "SHOW TABLE STATUS FROM " . $q->quote($self->{db})
298- . " LIKE \'$tbl\'";
299- PTDEBUG && _d($sql);
300- $engine = $dbh->selectrow_hashref($sql)->{engine};
301- PTDEBUG && _d($tbl, 'uses', $engine, 'engine');
302- }
303-
304-
305- if ( !$engine || $self->engine_is_allowed($engine) ) {
306- my $ddl;
307- if ( my $du = $self->{MySQLDump} ) {
308- $ddl = $du->get_create_table($dbh, $q, $self->{db}, $tbl)->[1];
309- }
310-
311+ my $ddl = $tp->get_create_table($dbh, $self->{db}, $tbl);
312+ my $tbl_struct = $tp->parse($ddl);
313+ if ( $self->engine_is_allowed($tbl_struct->{engine}) ) {
314 return {
315- db => $self->{db},
316- tbl => $tbl,
317- ddl => $ddl,
318+ db => $self->{db},
319+ tbl => $tbl,
320+ name => $q->quote($self->{db}, $tbl),
321+ ddl => $ddl,
322+ tbl_struct => $tbl_struct,
323 };
324 }
325 }
326@@ -3476,6 +3514,10 @@
327
328 my $filter = $self->{filters};
329
330+ if ( $db eq 'mysql' && ($tbl eq 'general_log' || $tbl eq 'slow_log') ) {
331+ return 0;
332+ }
333+
334 if ( $filter->{'ignore-tables'}->{$tbl}
335 && ($filter->{'ignore-tables'}->{$tbl} eq '*'
336 || $filter->{'ignore-tables'}->{$tbl} eq $db) ) {
337@@ -3515,7 +3557,11 @@
338
339 sub engine_is_allowed {
340 my ( $self, $engine ) = @_;
341- die "I need an engine argument" unless $engine;
342+
343+ if ( !$engine ) {
344+ PTDEBUG && _d('No engine specified; allowing the table');
345+ return 1;
346+ }
347
348 $engine = lc $engine;
349
350@@ -3535,6 +3581,40 @@
351 return 1;
352 }
353
354+sub _resume_from_database {
355+ my ($self, $db) = @_;
356+
357+ return 1 unless $self->{resume}->{db};
358+
359+ if ( $db eq $self->{resume}->{db} ) {
360+ PTDEBUG && _d('At resume db', $db);
361+ delete $self->{resume}->{db};
362+ return 1;
363+ }
364+
365+ return 0;
366+}
367+
368+sub _resume_from_table {
369+ my ($self, $tbl) = @_;
370+
371+ return 1 unless $self->{resume}->{tbl};
372+
373+ if ( $tbl eq $self->{resume}->{tbl} ) {
374+ if ( !$self->{resume}->{after} ) {
375+ PTDEBUG && _d('Resuming from table', $tbl);
376+ delete $self->{resume}->{tbl};
377+ return 1;
378+ }
379+ else {
380+ PTDEBUG && _d('Resuming after table', $tbl);
381+ delete $self->{resume}->{tbl};
382+ }
383+ }
384+
385+ return 0;
386+}
387+
388 sub _d {
389 my ($package, undef, $line) = caller 0;
390 @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; }
391@@ -3644,11 +3724,10 @@
392 MySQLDump => $du,
393 TableParser => $tp,
394 Schema => $schema,
395- keep_ddl => 1,
396 );
397 TABLE:
398- while ( my $tbl = $schema_itr->next_schema_object() ) {
399- $tbl->{engine} = $tp->get_engine($tbl->{ddl});
400+ while ( my $tbl = $schema_itr->next() ) {
401+ $tbl->{engine} = $tbl->{tbl_struct}->{engine};
402
403 my ($keys, $clustered_key, $fks);
404 if ( $get_keys ) {
405
406=== added file 'bin/pt-fingerprint'
407--- bin/pt-fingerprint 1970-01-01 00:00:00 +0000
408+++ bin/pt-fingerprint 2012-03-31 16:07:24 +0000
409@@ -0,0 +1,2143 @@
410+#!/usr/bin/env perl
411+
412+# This program is part of Percona Toolkit: http://www.percona.com/software/
413+# See "COPYRIGHT, LICENSE, AND WARRANTY" at the end of this file for legal
414+# notices and disclaimers.
415+
416+use strict;
417+use warnings FATAL => 'all';
418+use constant MKDEBUG => $ENV{MKDEBUG} || 0;
419+
420+# ###########################################################################
421+# OptionParser package
422+# This package is a copy without comments from the original. The original
423+# with comments and its test file can be found in the Bazaar repository at,
424+# lib/OptionParser.pm
425+# t/lib/OptionParser.t
426+# See https://launchpad.net/percona-toolkit for more information.
427+# ###########################################################################
428+{
429+package OptionParser;
430+
431+use strict;
432+use warnings FATAL => 'all';
433+use English qw(-no_match_vars);
434+use constant PTDEBUG => $ENV{PTDEBUG} || 0;
435+
436+use List::Util qw(max);
437+use Getopt::Long;
438+
439+my $POD_link_re = '[LC]<"?([^">]+)"?>';
440+
441+sub new {
442+ my ( $class, %args ) = @_;
443+ my @required_args = qw();
444+ foreach my $arg ( @required_args ) {
445+ die "I need a $arg argument" unless $args{$arg};
446+ }
447+
448+ my ($program_name) = $PROGRAM_NAME =~ m/([.A-Za-z-]+)$/;
449+ $program_name ||= $PROGRAM_NAME;
450+ my $home = $ENV{HOME} || $ENV{HOMEPATH} || $ENV{USERPROFILE} || '.';
451+
452+ my %attributes = (
453+ 'type' => 1,
454+ 'short form' => 1,
455+ 'group' => 1,
456+ 'default' => 1,
457+ 'cumulative' => 1,
458+ 'negatable' => 1,
459+ );
460+
461+ my $self = {
462+ head1 => 'OPTIONS', # These args are used internally
463+ skip_rules => 0, # to instantiate another Option-
464+ item => '--(.*)', # Parser obj that parses the
465+ attributes => \%attributes, # DSN OPTIONS section. Tools
466+ parse_attributes => \&_parse_attribs, # don't tinker with these args.
467+
468+ %args,
469+
470+ strict => 1, # disabled by a special rule
471+ program_name => $program_name,
472+ opts => {},
473+ got_opts => 0,
474+ short_opts => {},
475+ defaults => {},
476+ groups => {},
477+ allowed_groups => {},
478+ errors => [],
479+ rules => [], # desc of rules for --help
480+ mutex => [], # rule: opts are mutually exclusive
481+ atleast1 => [], # rule: at least one opt is required
482+ disables => {}, # rule: opt disables other opts
483+ defaults_to => {}, # rule: opt defaults to value of other opt
484+ DSNParser => undef,
485+ default_files => [
486+ "/etc/percona-toolkit/percona-toolkit.conf",
487+ "/etc/percona-toolkit/$program_name.conf",
488+ "$home/.percona-toolkit.conf",
489+ "$home/.$program_name.conf",
490+ ],
491+ types => {
492+ string => 's', # standard Getopt type
493+ int => 'i', # standard Getopt type
494+ float => 'f', # standard Getopt type
495+ Hash => 'H', # hash, formed from a comma-separated list
496+ hash => 'h', # hash as above, but only if a value is given
497+ Array => 'A', # array, similar to Hash
498+ array => 'a', # array, similar to hash
499+ DSN => 'd', # DSN
500+ size => 'z', # size with kMG suffix (powers of 2^10)
501+ time => 'm', # time, with an optional suffix of s/h/m/d
502+ },
503+ };
504+
505+ return bless $self, $class;
506+}
507+
508+sub get_specs {
509+ my ( $self, $file ) = @_;
510+ $file ||= $self->{file} || __FILE__;
511+ my @specs = $self->_pod_to_specs($file);
512+ $self->_parse_specs(@specs);
513+
514+ open my $fh, "<", $file or die "Cannot open $file: $OS_ERROR";
515+ my $contents = do { local $/ = undef; <$fh> };
516+ close $fh;
517+ if ( $contents =~ m/^=head1 DSN OPTIONS/m ) {
518+ PTDEBUG && _d('Parsing DSN OPTIONS');
519+ my $dsn_attribs = {
520+ dsn => 1,
521+ copy => 1,
522+ };
523+ my $parse_dsn_attribs = sub {
524+ my ( $self, $option, $attribs ) = @_;
525+ map {
526+ my $val = $attribs->{$_};
527+ if ( $val ) {
528+ $val = $val eq 'yes' ? 1
529+ : $val eq 'no' ? 0
530+ : $val;
531+ $attribs->{$_} = $val;
532+ }
533+ } keys %$attribs;
534+ return {
535+ key => $option,
536+ %$attribs,
537+ };
538+ };
539+ my $dsn_o = new OptionParser(
540+ description => 'DSN OPTIONS',
541+ head1 => 'DSN OPTIONS',
542+ dsn => 0, # XXX don't infinitely recurse!
543+ item => '\* (.)', # key opts are a single character
544+ skip_rules => 1, # no rules before opts
545+ attributes => $dsn_attribs,
546+ parse_attributes => $parse_dsn_attribs,
547+ );
548+ my @dsn_opts = map {
549+ my $opts = {
550+ key => $_->{spec}->{key},
551+ dsn => $_->{spec}->{dsn},
552+ copy => $_->{spec}->{copy},
553+ desc => $_->{desc},
554+ };
555+ $opts;
556+ } $dsn_o->_pod_to_specs($file);
557+ $self->{DSNParser} = DSNParser->new(opts => \@dsn_opts);
558+ }
559+
560+ if ( $contents =~ m/^=head1 VERSION\n\n^(.+)$/m ) {
561+ $self->{version} = $1;
562+ PTDEBUG && _d($self->{version});
563+ }
564+
565+ return;
566+}
567+
568+sub DSNParser {
569+ my ( $self ) = @_;
570+ return $self->{DSNParser};
571+};
572+
573+sub get_defaults_files {
574+ my ( $self ) = @_;
575+ return @{$self->{default_files}};
576+}
577+
578+sub _pod_to_specs {
579+ my ( $self, $file ) = @_;
580+ $file ||= $self->{file} || __FILE__;
581+ open my $fh, '<', $file or die "Cannot open $file: $OS_ERROR";
582+
583+ my @specs = ();
584+ my @rules = ();
585+ my $para;
586+
587+ local $INPUT_RECORD_SEPARATOR = '';
588+ while ( $para = <$fh> ) {
589+ next unless $para =~ m/^=head1 $self->{head1}/;
590+ last;
591+ }
592+
593+ while ( $para = <$fh> ) {
594+ last if $para =~ m/^=over/;
595+ next if $self->{skip_rules};
596+ chomp $para;
597+ $para =~ s/\s+/ /g;
598+ $para =~ s/$POD_link_re/$1/go;
599+ PTDEBUG && _d('Option rule:', $para);
600+ push @rules, $para;
601+ }
602+
603+ die "POD has no $self->{head1} section" unless $para;
604+
605+ do {
606+ if ( my ($option) = $para =~ m/^=item $self->{item}/ ) {
607+ chomp $para;
608+ PTDEBUG && _d($para);
609+ my %attribs;
610+
611+ $para = <$fh>; # read next paragraph, possibly attributes
612+
613+ if ( $para =~ m/: / ) { # attributes
614+ $para =~ s/\s+\Z//g;
615+ %attribs = map {
616+ my ( $attrib, $val) = split(/: /, $_);
617+ die "Unrecognized attribute for --$option: $attrib"
618+ unless $self->{attributes}->{$attrib};
619+ ($attrib, $val);
620+ } split(/; /, $para);
621+ if ( $attribs{'short form'} ) {
622+ $attribs{'short form'} =~ s/-//;
623+ }
624+ $para = <$fh>; # read next paragraph, probably short help desc
625+ }
626+ else {
627+ PTDEBUG && _d('Option has no attributes');
628+ }
629+
630+ $para =~ s/\s+\Z//g;
631+ $para =~ s/\s+/ /g;
632+ $para =~ s/$POD_link_re/$1/go;
633+
634+ $para =~ s/\.(?:\n.*| [A-Z].*|\Z)//s;
635+ PTDEBUG && _d('Short help:', $para);
636+
637+ die "No description after option spec $option" if $para =~ m/^=item/;
638+
639+ if ( my ($base_option) = $option =~ m/^\[no\](.*)/ ) {
640+ $option = $base_option;
641+ $attribs{'negatable'} = 1;
642+ }
643+
644+ push @specs, {
645+ spec => $self->{parse_attributes}->($self, $option, \%attribs),
646+ desc => $para
647+ . (defined $attribs{default} ? " (default $attribs{default})" : ''),
648+ group => ($attribs{'group'} ? $attribs{'group'} : 'default'),
649+ };
650+ }
651+ while ( $para = <$fh> ) {
652+ last unless $para;
653+ if ( $para =~ m/^=head1/ ) {
654+ $para = undef; # Can't 'last' out of a do {} block.
655+ last;
656+ }
657+ last if $para =~ m/^=item /;
658+ }
659+ } while ( $para );
660+
661+ die "No valid specs in $self->{head1}" unless @specs;
662+
663+ close $fh;
664+ return @specs, @rules;
665+}
666+
667+sub _parse_specs {
668+ my ( $self, @specs ) = @_;
669+ my %disables; # special rule that requires deferred checking
670+
671+ foreach my $opt ( @specs ) {
672+ if ( ref $opt ) { # It's an option spec, not a rule.
673+ PTDEBUG && _d('Parsing opt spec:',
674+ map { ($_, '=>', $opt->{$_}) } keys %$opt);
675+
676+ my ( $long, $short ) = $opt->{spec} =~ m/^([\w-]+)(?:\|([^!+=]*))?/;
677+ if ( !$long ) {
678+ die "Cannot parse long option from spec $opt->{spec}";
679+ }
680+ $opt->{long} = $long;
681+
682+ die "Duplicate long option --$long" if exists $self->{opts}->{$long};
683+ $self->{opts}->{$long} = $opt;
684+
685+ if ( length $long == 1 ) {
686+ PTDEBUG && _d('Long opt', $long, 'looks like short opt');
687+ $self->{short_opts}->{$long} = $long;
688+ }
689+
690+ if ( $short ) {
691+ die "Duplicate short option -$short"
692+ if exists $self->{short_opts}->{$short};
693+ $self->{short_opts}->{$short} = $long;
694+ $opt->{short} = $short;
695+ }
696+ else {
697+ $opt->{short} = undef;
698+ }
699+
700+ $opt->{is_negatable} = $opt->{spec} =~ m/!/ ? 1 : 0;
701+ $opt->{is_cumulative} = $opt->{spec} =~ m/\+/ ? 1 : 0;
702+ $opt->{is_required} = $opt->{desc} =~ m/required/ ? 1 : 0;
703+
704+ $opt->{group} ||= 'default';
705+ $self->{groups}->{ $opt->{group} }->{$long} = 1;
706+
707+ $opt->{value} = undef;
708+ $opt->{got} = 0;
709+
710+ my ( $type ) = $opt->{spec} =~ m/=(.)/;
711+ $opt->{type} = $type;
712+ PTDEBUG && _d($long, 'type:', $type);
713+
714+
715+ $opt->{spec} =~ s/=./=s/ if ( $type && $type =~ m/[HhAadzm]/ );
716+
717+ if ( (my ($def) = $opt->{desc} =~ m/default\b(?: ([^)]+))?/) ) {
718+ $self->{defaults}->{$long} = defined $def ? $def : 1;
719+ PTDEBUG && _d($long, 'default:', $def);
720+ }
721+
722+ if ( $long eq 'config' ) {
723+ $self->{defaults}->{$long} = join(',', $self->get_defaults_files());
724+ }
725+
726+ if ( (my ($dis) = $opt->{desc} =~ m/(disables .*)/) ) {
727+ $disables{$long} = $dis;
728+ PTDEBUG && _d('Deferring check of disables rule for', $opt, $dis);
729+ }
730+
731+ $self->{opts}->{$long} = $opt;
732+ }
733+ else { # It's an option rule, not a spec.
734+ PTDEBUG && _d('Parsing rule:', $opt);
735+ push @{$self->{rules}}, $opt;
736+ my @participants = $self->_get_participants($opt);
737+ my $rule_ok = 0;
738+
739+ if ( $opt =~ m/mutually exclusive|one and only one/ ) {
740+ $rule_ok = 1;
741+ push @{$self->{mutex}}, \@participants;
742+ PTDEBUG && _d(@participants, 'are mutually exclusive');
743+ }
744+ if ( $opt =~ m/at least one|one and only one/ ) {
745+ $rule_ok = 1;
746+ push @{$self->{atleast1}}, \@participants;
747+ PTDEBUG && _d(@participants, 'require at least one');
748+ }
749+ if ( $opt =~ m/default to/ ) {
750+ $rule_ok = 1;
751+ $self->{defaults_to}->{$participants[0]} = $participants[1];
752+ PTDEBUG && _d($participants[0], 'defaults to', $participants[1]);
753+ }
754+ if ( $opt =~ m/restricted to option groups/ ) {
755+ $rule_ok = 1;
756+ my ($groups) = $opt =~ m/groups ([\w\s\,]+)/;
757+ my @groups = split(',', $groups);
758+ %{$self->{allowed_groups}->{$participants[0]}} = map {
759+ s/\s+//;
760+ $_ => 1;
761+ } @groups;
762+ }
763+ if( $opt =~ m/accepts additional command-line arguments/ ) {
764+ $rule_ok = 1;
765+ $self->{strict} = 0;
766+ PTDEBUG && _d("Strict mode disabled by rule");
767+ }
768+
769+ die "Unrecognized option rule: $opt" unless $rule_ok;
770+ }
771+ }
772+
773+ foreach my $long ( keys %disables ) {
774+ my @participants = $self->_get_participants($disables{$long});
775+ $self->{disables}->{$long} = \@participants;
776+ PTDEBUG && _d('Option', $long, 'disables', @participants);
777+ }
778+
779+ return;
780+}
781+
782+sub _get_participants {
783+ my ( $self, $str ) = @_;
784+ my @participants;
785+ foreach my $long ( $str =~ m/--(?:\[no\])?([\w-]+)/g ) {
786+ die "Option --$long does not exist while processing rule $str"
787+ unless exists $self->{opts}->{$long};
788+ push @participants, $long;
789+ }
790+ PTDEBUG && _d('Participants for', $str, ':', @participants);
791+ return @participants;
792+}
793+
794+sub opts {
795+ my ( $self ) = @_;
796+ my %opts = %{$self->{opts}};
797+ return %opts;
798+}
799+
800+sub short_opts {
801+ my ( $self ) = @_;
802+ my %short_opts = %{$self->{short_opts}};
803+ return %short_opts;
804+}
805+
806+sub set_defaults {
807+ my ( $self, %defaults ) = @_;
808+ $self->{defaults} = {};
809+ foreach my $long ( keys %defaults ) {
810+ die "Cannot set default for nonexistent option $long"
811+ unless exists $self->{opts}->{$long};
812+ $self->{defaults}->{$long} = $defaults{$long};
813+ PTDEBUG && _d('Default val for', $long, ':', $defaults{$long});
814+ }
815+ return;
816+}
817+
818+sub get_defaults {
819+ my ( $self ) = @_;
820+ return $self->{defaults};
821+}
822+
823+sub get_groups {
824+ my ( $self ) = @_;
825+ return $self->{groups};
826+}
827+
828+sub _set_option {
829+ my ( $self, $opt, $val ) = @_;
830+ my $long = exists $self->{opts}->{$opt} ? $opt
831+ : exists $self->{short_opts}->{$opt} ? $self->{short_opts}->{$opt}
832+ : die "Getopt::Long gave a nonexistent option: $opt";
833+
834+ $opt = $self->{opts}->{$long};
835+ if ( $opt->{is_cumulative} ) {
836+ $opt->{value}++;
837+ }
838+ else {
839+ $opt->{value} = $val;
840+ }
841+ $opt->{got} = 1;
842+ PTDEBUG && _d('Got option', $long, '=', $val);
843+}
844+
845+sub get_opts {
846+ my ( $self ) = @_;
847+
848+ foreach my $long ( keys %{$self->{opts}} ) {
849+ $self->{opts}->{$long}->{got} = 0;
850+ $self->{opts}->{$long}->{value}
851+ = exists $self->{defaults}->{$long} ? $self->{defaults}->{$long}
852+ : $self->{opts}->{$long}->{is_cumulative} ? 0
853+ : undef;
854+ }
855+ $self->{got_opts} = 0;
856+
857+ $self->{errors} = [];
858+
859+ if ( @ARGV && $ARGV[0] eq "--config" ) {
860+ shift @ARGV;
861+ $self->_set_option('config', shift @ARGV);
862+ }
863+ if ( $self->has('config') ) {
864+ my @extra_args;
865+ foreach my $filename ( split(',', $self->get('config')) ) {
866+ eval {
867+ push @extra_args, $self->_read_config_file($filename);
868+ };
869+ if ( $EVAL_ERROR ) {
870+ if ( $self->got('config') ) {
871+ die $EVAL_ERROR;
872+ }
873+ elsif ( PTDEBUG ) {
874+ _d($EVAL_ERROR);
875+ }
876+ }
877+ }
878+ unshift @ARGV, @extra_args;
879+ }
880+
881+ Getopt::Long::Configure('no_ignore_case', 'bundling');
882+ GetOptions(
883+ map { $_->{spec} => sub { $self->_set_option(@_); } }
884+ grep { $_->{long} ne 'config' } # --config is handled specially above.
885+ values %{$self->{opts}}
886+ ) or $self->save_error('Error parsing options');
887+
888+ if ( exists $self->{opts}->{version} && $self->{opts}->{version}->{got} ) {
889+ if ( $self->{version} ) {
890+ print $self->{version}, "\n";
891+ }
892+ else {
893+ print "Error parsing version. See the VERSION section of the tool's documentation.\n";
894+ }
895+ exit 0;
896+ }
897+
898+ if ( @ARGV && $self->{strict} ) {
899+ $self->save_error("Unrecognized command-line options @ARGV");
900+ }
901+
902+ foreach my $mutex ( @{$self->{mutex}} ) {
903+ my @set = grep { $self->{opts}->{$_}->{got} } @$mutex;
904+ if ( @set > 1 ) {
905+ my $err = join(', ', map { "--$self->{opts}->{$_}->{long}" }
906+ @{$mutex}[ 0 .. scalar(@$mutex) - 2] )
907+ . ' and --'.$self->{opts}->{$mutex->[-1]}->{long}
908+ . ' are mutually exclusive.';
909+ $self->save_error($err);
910+ }
911+ }
912+
913+ foreach my $required ( @{$self->{atleast1}} ) {
914+ my @set = grep { $self->{opts}->{$_}->{got} } @$required;
915+ if ( @set == 0 ) {
916+ my $err = join(', ', map { "--$self->{opts}->{$_}->{long}" }
917+ @{$required}[ 0 .. scalar(@$required) - 2] )
918+ .' or --'.$self->{opts}->{$required->[-1]}->{long};
919+ $self->save_error("Specify at least one of $err");
920+ }
921+ }
922+
923+ $self->_check_opts( keys %{$self->{opts}} );
924+ $self->{got_opts} = 1;
925+ return;
926+}
927+
928+sub _check_opts {
929+ my ( $self, @long ) = @_;
930+ my $long_last = scalar @long;
931+ while ( @long ) {
932+ foreach my $i ( 0..$#long ) {
933+ my $long = $long[$i];
934+ next unless $long;
935+ my $opt = $self->{opts}->{$long};
936+ if ( $opt->{got} ) {
937+ if ( exists $self->{disables}->{$long} ) {
938+ my @disable_opts = @{$self->{disables}->{$long}};
939+ map { $self->{opts}->{$_}->{value} = undef; } @disable_opts;
940+ PTDEBUG && _d('Unset options', @disable_opts,
941+ 'because', $long,'disables them');
942+ }
943+
944+ if ( exists $self->{allowed_groups}->{$long} ) {
945+
946+ my @restricted_groups = grep {
947+ !exists $self->{allowed_groups}->{$long}->{$_}
948+ } keys %{$self->{groups}};
949+
950+ my @restricted_opts;
951+ foreach my $restricted_group ( @restricted_groups ) {
952+ RESTRICTED_OPT:
953+ foreach my $restricted_opt (
954+ keys %{$self->{groups}->{$restricted_group}} )
955+ {
956+ next RESTRICTED_OPT if $restricted_opt eq $long;
957+ push @restricted_opts, $restricted_opt
958+ if $self->{opts}->{$restricted_opt}->{got};
959+ }
960+ }
961+
962+ if ( @restricted_opts ) {
963+ my $err;
964+ if ( @restricted_opts == 1 ) {
965+ $err = "--$restricted_opts[0]";
966+ }
967+ else {
968+ $err = join(', ',
969+ map { "--$self->{opts}->{$_}->{long}" }
970+ grep { $_ }
971+ @restricted_opts[0..scalar(@restricted_opts) - 2]
972+ )
973+ . ' or --'.$self->{opts}->{$restricted_opts[-1]}->{long};
974+ }
975+ $self->save_error("--$long is not allowed with $err");
976+ }
977+ }
978+
979+ }
980+ elsif ( $opt->{is_required} ) {
981+ $self->save_error("Required option --$long must be specified");
982+ }
983+
984+ $self->_validate_type($opt);
985+ if ( $opt->{parsed} ) {
986+ delete $long[$i];
987+ }
988+ else {
989+ PTDEBUG && _d('Temporarily failed to parse', $long);
990+ }
991+ }
992+
993+ die "Failed to parse options, possibly due to circular dependencies"
994+ if @long == $long_last;
995+ $long_last = @long;
996+ }
997+
998+ return;
999+}
1000+
1001+sub _validate_type {
1002+ my ( $self, $opt ) = @_;
1003+ return unless $opt;
1004+
1005+ if ( !$opt->{type} ) {
1006+ $opt->{parsed} = 1;
1007+ return;
1008+ }
1009+
1010+ my $val = $opt->{value};
1011+
1012+ if ( $val && $opt->{type} eq 'm' ) { # type time
1013+ PTDEBUG && _d('Parsing option', $opt->{long}, 'as a time value');
1014+ my ( $prefix, $num, $suffix ) = $val =~ m/([+-]?)(\d+)([a-z])?$/;
1015+ if ( !$suffix ) {
1016+ my ( $s ) = $opt->{desc} =~ m/\(suffix (.)\)/;
1017+ $suffix = $s || 's';
1018+ PTDEBUG && _d('No suffix given; using', $suffix, 'for',
1019+ $opt->{long}, '(value:', $val, ')');
1020+ }
1021+ if ( $suffix =~ m/[smhd]/ ) {
1022+ $val = $suffix eq 's' ? $num # Seconds
1023+ : $suffix eq 'm' ? $num * 60 # Minutes
1024+ : $suffix eq 'h' ? $num * 3600 # Hours
1025+ : $num * 86400; # Days
1026+ $opt->{value} = ($prefix || '') . $val;
1027+ PTDEBUG && _d('Setting option', $opt->{long}, 'to', $val);
1028+ }
1029+ else {
1030+ $self->save_error("Invalid time suffix for --$opt->{long}");
1031+ }
1032+ }
1033+ elsif ( $val && $opt->{type} eq 'd' ) { # type DSN
1034+ PTDEBUG && _d('Parsing option', $opt->{long}, 'as a DSN');
1035+ my $prev = {};
1036+ my $from_key = $self->{defaults_to}->{ $opt->{long} };
1037+ if ( $from_key ) {
1038+ PTDEBUG && _d($opt->{long}, 'DSN copies from', $from_key, 'DSN');
1039+ if ( $self->{opts}->{$from_key}->{parsed} ) {
1040+ $prev = $self->{opts}->{$from_key}->{value};
1041+ }
1042+ else {
1043+ PTDEBUG && _d('Cannot parse', $opt->{long}, 'until',
1044+ $from_key, 'parsed');
1045+ return;
1046+ }
1047+ }
1048+ my $defaults = $self->{DSNParser}->parse_options($self);
1049+ $opt->{value} = $self->{DSNParser}->parse($val, $prev, $defaults);
1050+ }
1051+ elsif ( $val && $opt->{type} eq 'z' ) { # type size
1052+ PTDEBUG && _d('Parsing option', $opt->{long}, 'as a size value');
1053+ $self->_parse_size($opt, $val);
1054+ }
1055+ elsif ( $opt->{type} eq 'H' || (defined $val && $opt->{type} eq 'h') ) {
1056+ $opt->{value} = { map { $_ => 1 } split(/(?<!\\),\s*/, ($val || '')) };
1057+ }
1058+ elsif ( $opt->{type} eq 'A' || (defined $val && $opt->{type} eq 'a') ) {
1059+ $opt->{value} = [ split(/(?<!\\),\s*/, ($val || '')) ];
1060+ }
1061+ else {
1062+ PTDEBUG && _d('Nothing to validate for option',
1063+ $opt->{long}, 'type', $opt->{type}, 'value', $val);
1064+ }
1065+
1066+ $opt->{parsed} = 1;
1067+ return;
1068+}
1069+
1070+sub get {
1071+ my ( $self, $opt ) = @_;
1072+ my $long = (length $opt == 1 ? $self->{short_opts}->{$opt} : $opt);
1073+ die "Option $opt does not exist"
1074+ unless $long && exists $self->{opts}->{$long};
1075+ return $self->{opts}->{$long}->{value};
1076+}
1077+
1078+sub got {
1079+ my ( $self, $opt ) = @_;
1080+ my $long = (length $opt == 1 ? $self->{short_opts}->{$opt} : $opt);
1081+ die "Option $opt does not exist"
1082+ unless $long && exists $self->{opts}->{$long};
1083+ return $self->{opts}->{$long}->{got};
1084+}
1085+
1086+sub has {
1087+ my ( $self, $opt ) = @_;
1088+ my $long = (length $opt == 1 ? $self->{short_opts}->{$opt} : $opt);
1089+ return defined $long ? exists $self->{opts}->{$long} : 0;
1090+}
1091+
1092+sub set {
1093+ my ( $self, $opt, $val ) = @_;
1094+ my $long = (length $opt == 1 ? $self->{short_opts}->{$opt} : $opt);
1095+ die "Option $opt does not exist"
1096+ unless $long && exists $self->{opts}->{$long};
1097+ $self->{opts}->{$long}->{value} = $val;
1098+ return;
1099+}
1100+
1101+sub save_error {
1102+ my ( $self, $error ) = @_;
1103+ push @{$self->{errors}}, $error;
1104+ return;
1105+}
1106+
1107+sub errors {
1108+ my ( $self ) = @_;
1109+ return $self->{errors};
1110+}
1111+
1112+sub usage {
1113+ my ( $self ) = @_;
1114+ warn "No usage string is set" unless $self->{usage}; # XXX
1115+ return "Usage: " . ($self->{usage} || '') . "\n";
1116+}
1117+
1118+sub descr {
1119+ my ( $self ) = @_;
1120+ warn "No description string is set" unless $self->{description}; # XXX
1121+ my $descr = ($self->{description} || $self->{program_name} || '')
1122+ . " For more details, please use the --help option, "
1123+ . "or try 'perldoc $PROGRAM_NAME' "
1124+ . "for complete documentation.";
1125+ $descr = join("\n", $descr =~ m/(.{0,80})(?:\s+|$)/g)
1126+ unless $ENV{DONT_BREAK_LINES};
1127+ $descr =~ s/ +$//mg;
1128+ return $descr;
1129+}
1130+
1131+sub usage_or_errors {
1132+ my ( $self, $file, $return ) = @_;
1133+ $file ||= $self->{file} || __FILE__;
1134+
1135+ if ( !$self->{description} || !$self->{usage} ) {
1136+ PTDEBUG && _d("Getting description and usage from SYNOPSIS in", $file);
1137+ my %synop = $self->_parse_synopsis($file);
1138+ $self->{description} ||= $synop{description};
1139+ $self->{usage} ||= $synop{usage};
1140+ PTDEBUG && _d("Description:", $self->{description},
1141+ "\nUsage:", $self->{usage});
1142+ }
1143+
1144+ if ( $self->{opts}->{help}->{got} ) {
1145+ print $self->print_usage() or die "Cannot print usage: $OS_ERROR";
1146+ exit 0 unless $return;
1147+ }
1148+ elsif ( scalar @{$self->{errors}} ) {
1149+ print $self->print_errors() or die "Cannot print errors: $OS_ERROR";
1150+ exit 0 unless $return;
1151+ }
1152+
1153+ return;
1154+}
1155+
1156+sub print_errors {
1157+ my ( $self ) = @_;
1158+ my $usage = $self->usage() . "\n";
1159+ if ( (my @errors = @{$self->{errors}}) ) {
1160+ $usage .= join("\n * ", 'Errors in command-line arguments:', @errors)
1161+ . "\n";
1162+ }
1163+ return $usage . "\n" . $self->descr();
1164+}
1165+
1166+sub print_usage {
1167+ my ( $self ) = @_;
1168+ die "Run get_opts() before print_usage()" unless $self->{got_opts};
1169+ my @opts = values %{$self->{opts}};
1170+
1171+ my $maxl = max(
1172+ map {
1173+ length($_->{long}) # option long name
1174+ + ($_->{is_negatable} ? 4 : 0) # "[no]" if opt is negatable
1175+ + ($_->{type} ? 2 : 0) # "=x" where x is the opt type
1176+ }
1177+ @opts);
1178+
1179+ my $maxs = max(0,
1180+ map {
1181+ length($_)
1182+ + ($self->{opts}->{$_}->{is_negatable} ? 4 : 0)
1183+ + ($self->{opts}->{$_}->{type} ? 2 : 0)
1184+ }
1185+ values %{$self->{short_opts}});
1186+
1187+ my $lcol = max($maxl, ($maxs + 3));
1188+ my $rcol = 80 - $lcol - 6;
1189+ my $rpad = ' ' x ( 80 - $rcol );
1190+
1191+ $maxs = max($lcol - 3, $maxs);
1192+
1193+ my $usage = $self->descr() . "\n" . $self->usage();
1194+
1195+ my @groups = reverse sort grep { $_ ne 'default'; } keys %{$self->{groups}};
1196+ push @groups, 'default';
1197+
1198+ foreach my $group ( reverse @groups ) {
1199+ $usage .= "\n".($group eq 'default' ? 'Options' : $group).":\n\n";
1200+ foreach my $opt (
1201+ sort { $a->{long} cmp $b->{long} }
1202+ grep { $_->{group} eq $group }
1203+ @opts )
1204+ {
1205+ my $long = $opt->{is_negatable} ? "[no]$opt->{long}" : $opt->{long};
1206+ my $short = $opt->{short};
1207+ my $desc = $opt->{desc};
1208+
1209+ $long .= $opt->{type} ? "=$opt->{type}" : "";
1210+
1211+ if ( $opt->{type} && $opt->{type} eq 'm' ) {
1212+ my ($s) = $desc =~ m/\(suffix (.)\)/;
1213+ $s ||= 's';
1214+ $desc =~ s/\s+\(suffix .\)//;
1215+ $desc .= ". Optional suffix s=seconds, m=minutes, h=hours, "
1216+ . "d=days; if no suffix, $s is used.";
1217+ }
1218+ $desc = join("\n$rpad", grep { $_ } $desc =~ m/(.{0,$rcol})(?:\s+|$)/g);
1219+ $desc =~ s/ +$//mg;
1220+ if ( $short ) {
1221+ $usage .= sprintf(" --%-${maxs}s -%s %s\n", $long, $short, $desc);
1222+ }
1223+ else {
1224+ $usage .= sprintf(" --%-${lcol}s %s\n", $long, $desc);
1225+ }
1226+ }
1227+ }
1228+
1229+ $usage .= "\nOption types: s=string, i=integer, f=float, h/H/a/A=comma-separated list, d=DSN, z=size, m=time\n";
1230+
1231+ if ( (my @rules = @{$self->{rules}}) ) {
1232+ $usage .= "\nRules:\n\n";
1233+ $usage .= join("\n", map { " $_" } @rules) . "\n";
1234+ }
1235+ if ( $self->{DSNParser} ) {
1236+ $usage .= "\n" . $self->{DSNParser}->usage();
1237+ }
1238+ $usage .= "\nOptions and values after processing arguments:\n\n";
1239+ foreach my $opt ( sort { $a->{long} cmp $b->{long} } @opts ) {
1240+ my $val = $opt->{value};
1241+ my $type = $opt->{type} || '';
1242+ my $bool = $opt->{spec} =~ m/^[\w-]+(?:\|[\w-])?!?$/;
1243+ $val = $bool ? ( $val ? 'TRUE' : 'FALSE' )
1244+ : !defined $val ? '(No value)'
1245+ : $type eq 'd' ? $self->{DSNParser}->as_string($val)
1246+ : $type =~ m/H|h/ ? join(',', sort keys %$val)
1247+ : $type =~ m/A|a/ ? join(',', @$val)
1248+ : $val;
1249+ $usage .= sprintf(" --%-${lcol}s %s\n", $opt->{long}, $val);
1250+ }
1251+ return $usage;
1252+}
1253+
1254+sub prompt_noecho {
1255+ shift @_ if ref $_[0] eq __PACKAGE__;
1256+ my ( $prompt ) = @_;
1257+ local $OUTPUT_AUTOFLUSH = 1;
1258+ print $prompt
1259+ or die "Cannot print: $OS_ERROR";
1260+ my $response;
1261+ eval {
1262+ require Term::ReadKey;
1263+ Term::ReadKey::ReadMode('noecho');
1264+ chomp($response = <STDIN>);
1265+ Term::ReadKey::ReadMode('normal');
1266+ print "\n"
1267+ or die "Cannot print: $OS_ERROR";
1268+ };
1269+ if ( $EVAL_ERROR ) {
1270+ die "Cannot read response; is Term::ReadKey installed? $EVAL_ERROR";
1271+ }
1272+ return $response;
1273+}
1274+
1275+sub _read_config_file {
1276+ my ( $self, $filename ) = @_;
1277+ open my $fh, "<", $filename or die "Cannot open $filename: $OS_ERROR\n";
1278+ my @args;
1279+ my $prefix = '--';
1280+ my $parse = 1;
1281+
1282+ LINE:
1283+ while ( my $line = <$fh> ) {
1284+ chomp $line;
1285+ next LINE if $line =~ m/^\s*(?:\#|\;|$)/;
1286+ $line =~ s/\s+#.*$//g;
1287+ $line =~ s/^\s+|\s+$//g;
1288+ if ( $line eq '--' ) {
1289+ $prefix = '';
1290+ $parse = 0;
1291+ next LINE;
1292+ }
1293+ if ( $parse
1294+ && (my($opt, $arg) = $line =~ m/^\s*([^=\s]+?)(?:\s*=\s*(.*?)\s*)?$/)
1295+ ) {
1296+ push @args, grep { defined $_ } ("$prefix$opt", $arg);
1297+ }
1298+ elsif ( $line =~ m/./ ) {
1299+ push @args, $line;
1300+ }
1301+ else {
1302+ die "Syntax error in file $filename at line $INPUT_LINE_NUMBER";
1303+ }
1304+ }
1305+ close $fh;
1306+ return @args;
1307+}
1308+
1309+sub read_para_after {
1310+ my ( $self, $file, $regex ) = @_;
1311+ open my $fh, "<", $file or die "Can't open $file: $OS_ERROR";
1312+ local $INPUT_RECORD_SEPARATOR = '';
1313+ my $para;
1314+ while ( $para = <$fh> ) {
1315+ next unless $para =~ m/^=pod$/m;
1316+ last;
1317+ }
1318+ while ( $para = <$fh> ) {
1319+ next unless $para =~ m/$regex/;
1320+ last;
1321+ }
1322+ $para = <$fh>;
1323+ chomp($para);
1324+ close $fh or die "Can't close $file: $OS_ERROR";
1325+ return $para;
1326+}
1327+
1328+sub clone {
1329+ my ( $self ) = @_;
1330+
1331+ my %clone = map {
1332+ my $hashref = $self->{$_};
1333+ my $val_copy = {};
1334+ foreach my $key ( keys %$hashref ) {
1335+ my $ref = ref $hashref->{$key};
1336+ $val_copy->{$key} = !$ref ? $hashref->{$key}
1337+ : $ref eq 'HASH' ? { %{$hashref->{$key}} }
1338+ : $ref eq 'ARRAY' ? [ @{$hashref->{$key}} ]
1339+ : $hashref->{$key};
1340+ }
1341+ $_ => $val_copy;
1342+ } qw(opts short_opts defaults);
1343+
1344+ foreach my $scalar ( qw(got_opts) ) {
1345+ $clone{$scalar} = $self->{$scalar};
1346+ }
1347+
1348+ return bless \%clone;
1349+}
1350+
1351+sub _parse_size {
1352+ my ( $self, $opt, $val ) = @_;
1353+
1354+ if ( lc($val || '') eq 'null' ) {
1355+ PTDEBUG && _d('NULL size for', $opt->{long});
1356+ $opt->{value} = 'null';
1357+ return;
1358+ }
1359+
1360+ my %factor_for = (k => 1_024, M => 1_048_576, G => 1_073_741_824);
1361+ my ($pre, $num, $factor) = $val =~ m/^([+-])?(\d+)([kMG])?$/;
1362+ if ( defined $num ) {
1363+ if ( $factor ) {
1364+ $num *= $factor_for{$factor};
1365+ PTDEBUG && _d('Setting option', $opt->{y},
1366+ 'to num', $num, '* factor', $factor);
1367+ }
1368+ $opt->{value} = ($pre || '') . $num;
1369+ }
1370+ else {
1371+ $self->save_error("Invalid size for --$opt->{long}: $val");
1372+ }
1373+ return;
1374+}
1375+
1376+sub _parse_attribs {
1377+ my ( $self, $option, $attribs ) = @_;
1378+ my $types = $self->{types};
1379+ return $option
1380+ . ($attribs->{'short form'} ? '|' . $attribs->{'short form'} : '' )
1381+ . ($attribs->{'negatable'} ? '!' : '' )
1382+ . ($attribs->{'cumulative'} ? '+' : '' )
1383+ . ($attribs->{'type'} ? '=' . $types->{$attribs->{type}} : '' );
1384+}
1385+
1386+sub _parse_synopsis {
1387+ my ( $self, $file ) = @_;
1388+ $file ||= $self->{file} || __FILE__;
1389+ PTDEBUG && _d("Parsing SYNOPSIS in", $file);
1390+
1391+ local $INPUT_RECORD_SEPARATOR = ''; # read paragraphs
1392+ open my $fh, "<", $file or die "Cannot open $file: $OS_ERROR";
1393+ my $para;
1394+ 1 while defined($para = <$fh>) && $para !~ m/^=head1 SYNOPSIS/;
1395+ die "$file does not contain a SYNOPSIS section" unless $para;
1396+ my @synop;
1397+ for ( 1..2 ) { # 1 for the usage, 2 for the description
1398+ my $para = <$fh>;
1399+ push @synop, $para;
1400+ }
1401+ close $fh;
1402+ PTDEBUG && _d("Raw SYNOPSIS text:", @synop);
1403+ my ($usage, $desc) = @synop;
1404+ die "The SYNOPSIS section in $file is not formatted properly"
1405+ unless $usage && $desc;
1406+
1407+ $usage =~ s/^\s*Usage:\s+(.+)/$1/;
1408+ chomp $usage;
1409+
1410+ $desc =~ s/\n/ /g;
1411+ $desc =~ s/\s{2,}/ /g;
1412+ $desc =~ s/\. ([A-Z][a-z])/. $1/g;
1413+ $desc =~ s/\s+$//;
1414+
1415+ return (
1416+ description => $desc,
1417+ usage => $usage,
1418+ );
1419+};
1420+
1421+sub _d {
1422+ my ($package, undef, $line) = caller 0;
1423+ @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; }
1424+ map { defined $_ ? $_ : 'undef' }
1425+ @_;
1426+ print STDERR "# $package:$line $PID ", join(' ', @_), "\n";
1427+}
1428+
1429+if ( PTDEBUG ) {
1430+ print '# ', $^X, ' ', $], "\n";
1431+ if ( my $uname = `uname -a` ) {
1432+ $uname =~ s/\s+/ /g;
1433+ print "# $uname\n";
1434+ }
1435+ print '# Arguments: ',
1436+ join(' ', map { my $a = "_[$_]_"; $a =~ s/\n/\n# /g; $a; } @ARGV), "\n";
1437+}
1438+
1439+1;
1440+}
1441+# ###########################################################################
1442+# End OptionParser package
1443+# ###########################################################################
1444+
1445+# ###########################################################################
1446+# QueryParser package
1447+# This package is a copy without comments from the original. The original
1448+# with comments and its test file can be found in the Bazaar repository at,
1449+# lib/QueryParser.pm
1450+# t/lib/QueryParser.t
1451+# See https://launchpad.net/percona-toolkit for more information.
1452+# ###########################################################################
1453+{
1454+package QueryParser;
1455+
1456+use strict;
1457+use warnings FATAL => 'all';
1458+use English qw(-no_match_vars);
1459+use constant PTDEBUG => $ENV{PTDEBUG} || 0;
1460+
1461+our $tbl_ident = qr/(?:`[^`]+`|\w+)(?:\.(?:`[^`]+`|\w+))?/;
1462+our $tbl_regex = qr{
1463+ \b(?:FROM|JOIN|(?<!KEY\s)UPDATE|INTO) # Words that precede table names
1464+ \b\s*
1465+ \(? # Optional paren around tables
1466+ ($tbl_ident
1467+ (?: (?:\s+ (?:AS\s+)? \w+)?, \s*$tbl_ident )*
1468+ )
1469+ }xio;
1470+our $has_derived = qr{
1471+ \b(?:FROM|JOIN|,)
1472+ \s*\(\s*SELECT
1473+ }xi;
1474+
1475+our $data_def_stmts = qr/(?:CREATE|ALTER|TRUNCATE|DROP|RENAME)/i;
1476+
1477+our $data_manip_stmts = qr/(?:INSERT|UPDATE|DELETE|REPLACE)/i;
1478+
1479+sub new {
1480+ my ( $class ) = @_;
1481+ bless {}, $class;
1482+}
1483+
1484+sub get_tables {
1485+ my ( $self, $query ) = @_;
1486+ return unless $query;
1487+ PTDEBUG && _d('Getting tables for', $query);
1488+
1489+ my ( $ddl_stmt ) = $query =~ m/^\s*($data_def_stmts)\b/i;
1490+ if ( $ddl_stmt ) {
1491+ PTDEBUG && _d('Special table type:', $ddl_stmt);
1492+ $query =~ s/IF\s+(?:NOT\s+)?EXISTS//i;
1493+ if ( $query =~ m/$ddl_stmt DATABASE\b/i ) {
1494+ PTDEBUG && _d('Query alters a database, not a table');
1495+ return ();
1496+ }
1497+ if ( $ddl_stmt =~ m/CREATE/i && $query =~ m/$ddl_stmt\b.+?\bSELECT\b/i ) {
1498+ my ($select) = $query =~ m/\b(SELECT\b.+)/is;
1499+ PTDEBUG && _d('CREATE TABLE ... SELECT:', $select);
1500+ return $self->get_tables($select);
1501+ }
1502+ my ($tbl) = $query =~ m/TABLE\s+($tbl_ident)(\s+.*)?/i;
1503+ PTDEBUG && _d('Matches table:', $tbl);
1504+ return ($tbl);
1505+ }
1506+
1507+ $query =~ s/ (?:LOW_PRIORITY|IGNORE|STRAIGHT_JOIN)//ig;
1508+
1509+ if ( $query =~ /^\s*LOCK TABLES/i ) {
1510+ PTDEBUG && _d('Special table type: LOCK TABLES');
1511+ $query =~ s/^(\s*LOCK TABLES\s+)//;
1512+ $query =~ s/\s+(?:READ|WRITE|LOCAL)+\s*//g;
1513+ PTDEBUG && _d('Locked tables:', $query);
1514+ $query = "FROM $query";
1515+ }
1516+
1517+ $query =~ s/\\["']//g; # quoted strings
1518+ $query =~ s/".*?"/?/sg; # quoted strings
1519+ $query =~ s/'.*?'/?/sg; # quoted strings
1520+
1521+ my @tables;
1522+ foreach my $tbls ( $query =~ m/$tbl_regex/gio ) {
1523+ PTDEBUG && _d('Match tables:', $tbls);
1524+
1525+ next if $tbls =~ m/\ASELECT\b/i;
1526+
1527+ foreach my $tbl ( split(',', $tbls) ) {
1528+ $tbl =~ s/\s*($tbl_ident)(\s+.*)?/$1/gio;
1529+
1530+ if ( $tbl !~ m/[a-zA-Z]/ ) {
1531+ PTDEBUG && _d('Skipping suspicious table name:', $tbl);
1532+ next;
1533+ }
1534+
1535+ push @tables, $tbl;
1536+ }
1537+ }
1538+ return @tables;
1539+}
1540+
1541+sub has_derived_table {
1542+ my ( $self, $query ) = @_;
1543+ my $match = $query =~ m/$has_derived/;
1544+ PTDEBUG && _d($query, 'has ' . ($match ? 'a' : 'no') . ' derived table');
1545+ return $match;
1546+}
1547+
1548+sub get_aliases {
1549+ my ( $self, $query, $list ) = @_;
1550+
1551+ my $result = {
1552+ DATABASE => {},
1553+ TABLE => {},
1554+ };
1555+ return $result unless $query;
1556+
1557+ $query =~ s/ (?:LOW_PRIORITY|IGNORE|STRAIGHT_JOIN)//ig;
1558+
1559+ $query =~ s/ (?:INNER|OUTER|CROSS|LEFT|RIGHT|NATURAL)//ig;
1560+
1561+ my @tbl_refs;
1562+ my ($tbl_refs, $from) = $query =~ m{
1563+ (
1564+ (FROM|INTO|UPDATE)\b\s* # Keyword before table refs
1565+ .+? # Table refs
1566+ )
1567+ (?:\s+|\z) # If the query does not end with the table
1568+ (?:WHERE|ORDER|LIMIT|HAVING|SET|VALUES|\z) # Keyword after table refs
1569+ }ix;
1570+
1571+ if ( $tbl_refs ) {
1572+
1573+ if ( $query =~ m/^(?:INSERT|REPLACE)/i ) {
1574+ $tbl_refs =~ s/\([^\)]+\)\s*//;
1575+ }
1576+
1577+ PTDEBUG && _d('tbl refs:', $tbl_refs);
1578+
1579+ my $before_tbl = qr/(?:,|JOIN|\s|$from)+/i;
1580+
1581+ my $after_tbl = qr/(?:,|JOIN|ON|USING|\z)/i;
1582+
1583+ $tbl_refs =~ s/ = /=/g;
1584+
1585+ while (
1586+ $tbl_refs =~ m{
1587+ $before_tbl\b\s*
1588+ ( ($tbl_ident) (?:\s+ (?:AS\s+)? (\w+))? )
1589+ \s*$after_tbl
1590+ }xgio )
1591+ {
1592+ my ( $tbl_ref, $db_tbl, $alias ) = ($1, $2, $3);
1593+ PTDEBUG && _d('Match table:', $tbl_ref);
1594+ push @tbl_refs, $tbl_ref;
1595+ $alias = $self->trim_identifier($alias);
1596+
1597+ if ( $tbl_ref =~ m/^AS\s+\w+/i ) {
1598+ PTDEBUG && _d('Subquery', $tbl_ref);
1599+ $result->{TABLE}->{$alias} = undef;
1600+ next;
1601+ }
1602+
1603+ my ( $db, $tbl ) = $db_tbl =~ m/^(?:(.*?)\.)?(.*)/;
1604+ $db = $self->trim_identifier($db);
1605+ $tbl = $self->trim_identifier($tbl);
1606+ $result->{TABLE}->{$alias || $tbl} = $tbl;
1607+ $result->{DATABASE}->{$tbl} = $db if $db;
1608+ }
1609+ }
1610+ else {
1611+ PTDEBUG && _d("No tables ref in", $query);
1612+ }
1613+
1614+ if ( $list ) {
1615+ return \@tbl_refs;
1616+ }
1617+ else {
1618+ return $result;
1619+ }
1620+}
1621+
1622+sub split {
1623+ my ( $self, $query ) = @_;
1624+ return unless $query;
1625+ $query = $self->clean_query($query);
1626+ PTDEBUG && _d('Splitting', $query);
1627+
1628+ my $verbs = qr{SELECT|INSERT|UPDATE|DELETE|REPLACE|UNION|CREATE}i;
1629+
1630+ my @split_statements = grep { $_ } split(m/\b($verbs\b(?!(?:\s*\()))/io, $query);
1631+
1632+ my @statements;
1633+ if ( @split_statements == 1 ) {
1634+ push @statements, $query;
1635+ }
1636+ else {
1637+ for ( my $i = 0; $i <= $#split_statements; $i += 2 ) {
1638+ push @statements, $split_statements[$i].$split_statements[$i+1];
1639+
1640+ if ( $statements[-2] && $statements[-2] =~ m/on duplicate key\s+$/i ) {
1641+ $statements[-2] .= pop @statements;
1642+ }
1643+ }
1644+ }
1645+
1646+ PTDEBUG && _d('statements:', map { $_ ? "<$_>" : 'none' } @statements);
1647+ return @statements;
1648+}
1649+
1650+sub clean_query {
1651+ my ( $self, $query ) = @_;
1652+ return unless $query;
1653+ $query =~ s!/\*.*?\*/! !g; # Remove /* comment blocks */
1654+ $query =~ s/^\s+//; # Remove leading spaces
1655+ $query =~ s/\s+$//; # Remove trailing spaces
1656+ $query =~ s/\s{2,}/ /g; # Remove extra spaces
1657+ return $query;
1658+}
1659+
1660+sub split_subquery {
1661+ my ( $self, $query ) = @_;
1662+ return unless $query;
1663+ $query = $self->clean_query($query);
1664+ $query =~ s/;$//;
1665+
1666+ my @subqueries;
1667+ my $sqno = 0; # subquery number
1668+ my $pos = 0;
1669+ while ( $query =~ m/(\S+)(?:\s+|\Z)/g ) {
1670+ $pos = pos($query);
1671+ my $word = $1;
1672+ PTDEBUG && _d($word, $sqno);
1673+ if ( $word =~ m/^\(?SELECT\b/i ) {
1674+ my $start_pos = $pos - length($word) - 1;
1675+ if ( $start_pos ) {
1676+ $sqno++;
1677+ PTDEBUG && _d('Subquery', $sqno, 'starts at', $start_pos);
1678+ $subqueries[$sqno] = {
1679+ start_pos => $start_pos,
1680+ end_pos => 0,
1681+ len => 0,
1682+ words => [$word],
1683+ lp => 1, # left parentheses
1684+ rp => 0, # right parentheses
1685+ done => 0,
1686+ };
1687+ }
1688+ else {
1689+ PTDEBUG && _d('Main SELECT at pos 0');
1690+ }
1691+ }
1692+ else {
1693+ next unless $sqno; # next unless we're in a subquery
1694+ PTDEBUG && _d('In subquery', $sqno);
1695+ my $sq = $subqueries[$sqno];
1696+ if ( $sq->{done} ) {
1697+ PTDEBUG && _d('This subquery is done; SQL is for',
1698+ ($sqno - 1 ? "subquery $sqno" : "the main SELECT"));
1699+ next;
1700+ }
1701+ push @{$sq->{words}}, $word;
1702+ my $lp = ($word =~ tr/\(//) || 0;
1703+ my $rp = ($word =~ tr/\)//) || 0;
1704+ PTDEBUG && _d('parentheses left', $lp, 'right', $rp);
1705+ if ( ($sq->{lp} + $lp) - ($sq->{rp} + $rp) == 0 ) {
1706+ my $end_pos = $pos - 1;
1707+ PTDEBUG && _d('Subquery', $sqno, 'ends at', $end_pos);
1708+ $sq->{end_pos} = $end_pos;
1709+ $sq->{len} = $end_pos - $sq->{start_pos};
1710+ }
1711+ }
1712+ }
1713+
1714+ for my $i ( 1..$#subqueries ) {
1715+ my $sq = $subqueries[$i];
1716+ next unless $sq;
1717+ $sq->{sql} = join(' ', @{$sq->{words}});
1718+ substr $query,
1719+ $sq->{start_pos} + 1, # +1 for (
1720+ $sq->{len} - 1, # -1 for )
1721+ "__subquery_$i";
1722+ }
1723+
1724+ return $query, map { $_->{sql} } grep { defined $_ } @subqueries;
1725+}
1726+
1727+sub query_type {
1728+ my ( $self, $query, $qr ) = @_;
1729+ my ($type, undef) = $qr->distill_verbs($query);
1730+ my $rw;
1731+ if ( $type =~ m/^SELECT\b/ ) {
1732+ $rw = 'read';
1733+ }
1734+ elsif ( $type =~ m/^$data_manip_stmts\b/
1735+ || $type =~ m/^$data_def_stmts\b/ ) {
1736+ $rw = 'write'
1737+ }
1738+
1739+ return {
1740+ type => $type,
1741+ rw => $rw,
1742+ }
1743+}
1744+
1745+sub get_columns {
1746+ my ( $self, $query ) = @_;
1747+ my $cols = [];
1748+ return $cols unless $query;
1749+ my $cols_def;
1750+
1751+ if ( $query =~ m/^SELECT/i ) {
1752+ $query =~ s/
1753+ ^SELECT\s+
1754+ (?:ALL
1755+ |DISTINCT
1756+ |DISTINCTROW
1757+ |HIGH_PRIORITY
1758+ |STRAIGHT_JOIN
1759+ |SQL_SMALL_RESULT
1760+ |SQL_BIG_RESULT
1761+ |SQL_BUFFER_RESULT
1762+ |SQL_CACHE
1763+ |SQL_NO_CACHE
1764+ |SQL_CALC_FOUND_ROWS
1765+ )\s+
1766+ /SELECT /xgi;
1767+ ($cols_def) = $query =~ m/^SELECT\s+(.+?)\s+FROM/i;
1768+ }
1769+ elsif ( $query =~ m/^(?:INSERT|REPLACE)/i ) {
1770+ ($cols_def) = $query =~ m/\(([^\)]+)\)\s*VALUE/i;
1771+ }
1772+
1773+ PTDEBUG && _d('Columns:', $cols_def);
1774+ if ( $cols_def ) {
1775+ @$cols = split(',', $cols_def);
1776+ map {
1777+ my $col = $_;
1778+ $col = s/^\s+//g;
1779+ $col = s/\s+$//g;
1780+ $col;
1781+ } @$cols;
1782+ }
1783+
1784+ return $cols;
1785+}
1786+
1787+sub parse {
1788+ my ( $self, $query ) = @_;
1789+ return unless $query;
1790+ my $parsed = {};
1791+
1792+ $query =~ s/\n/ /g;
1793+ $query = $self->clean_query($query);
1794+
1795+ $parsed->{query} = $query,
1796+ $parsed->{tables} = $self->get_aliases($query, 1);
1797+ $parsed->{columns} = $self->get_columns($query);
1798+
1799+ my ($type) = $query =~ m/^(\w+)/;
1800+ $parsed->{type} = lc $type;
1801+
1802+
1803+ $parsed->{sub_queries} = [];
1804+
1805+ return $parsed;
1806+}
1807+
1808+sub extract_tables {
1809+ my ( $self, %args ) = @_;
1810+ my $query = $args{query};
1811+ my $default_db = $args{default_db};
1812+ my $q = $self->{Quoter} || $args{Quoter};
1813+ return unless $query;
1814+ PTDEBUG && _d('Extracting tables');
1815+ my @tables;
1816+ my %seen;
1817+ foreach my $db_tbl ( $self->get_tables($query) ) {
1818+ next unless $db_tbl;
1819+ next if $seen{$db_tbl}++; # Unique-ify for issue 337.
1820+ my ( $db, $tbl ) = $q->split_unquote($db_tbl);
1821+ push @tables, [ $db || $default_db, $tbl ];
1822+ }
1823+ return @tables;
1824+}
1825+
1826+sub trim_identifier {
1827+ my ($self, $str) = @_;
1828+ return unless defined $str;
1829+ $str =~ s/`//g;
1830+ $str =~ s/^\s+//;
1831+ $str =~ s/\s+$//;
1832+ return $str;
1833+}
1834+
1835+sub _d {
1836+ my ($package, undef, $line) = caller 0;
1837+ @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; }
1838+ map { defined $_ ? $_ : 'undef' }
1839+ @_;
1840+ print STDERR "# $package:$line $PID ", join(' ', @_), "\n";
1841+}
1842+
1843+1;
1844+}
1845+# ###########################################################################
1846+# End QueryParser package
1847+# ###########################################################################
1848+
1849+# ###########################################################################
1850+# QueryRewriter package
1851+# This package is a copy without comments from the original. The original
1852+# with comments and its test file can be found in the Bazaar repository at,
1853+# lib/QueryRewriter.pm
1854+# t/lib/QueryRewriter.t
1855+# See https://launchpad.net/percona-toolkit for more information.
1856+# ###########################################################################
1857+{
1858+package QueryRewriter;
1859+
1860+use strict;
1861+use warnings FATAL => 'all';
1862+use English qw(-no_match_vars);
1863+use constant PTDEBUG => $ENV{PTDEBUG} || 0;
1864+
1865+our $verbs = qr{^SHOW|^FLUSH|^COMMIT|^ROLLBACK|^BEGIN|SELECT|INSERT
1866+ |UPDATE|DELETE|REPLACE|^SET|UNION|^START|^LOCK}xi;
1867+my $quote_re = qr/"(?:(?!(?<!\\)").)*"|'(?:(?!(?<!\\)').)*'/; # Costly!
1868+my $bal;
1869+$bal = qr/
1870+ \(
1871+ (?:
1872+ (?> [^()]+ ) # Non-parens without backtracking
1873+ |
1874+ (??{ $bal }) # Group with matching parens
1875+ )*
1876+ \)
1877+ /x;
1878+
1879+my $olc_re = qr/(?:--|#)[^'"\r\n]*(?=[\r\n]|\Z)/; # One-line comments
1880+my $mlc_re = qr#/\*[^!].*?\*/#sm; # But not /*!version */
1881+my $vlc_re = qr#/\*.*?[0-9+].*?\*/#sm; # For SHOW + /*!version */
1882+my $vlc_rf = qr#^(SHOW).*?/\*![0-9+].*?\*/#sm; # Variation for SHOW
1883+
1884+
1885+sub new {
1886+ my ( $class, %args ) = @_;
1887+ my $self = { %args };
1888+ return bless $self, $class;
1889+}
1890+
1891+sub strip_comments {
1892+ my ( $self, $query ) = @_;
1893+ return unless $query;
1894+ $query =~ s/$olc_re//go;
1895+ $query =~ s/$mlc_re//go;
1896+ if ( $query =~ m/$vlc_rf/i ) { # contains show + version
1897+ $query =~ s/$vlc_re//go;
1898+ }
1899+ return $query;
1900+}
1901+
1902+sub shorten {
1903+ my ( $self, $query, $length ) = @_;
1904+ $query =~ s{
1905+ \A(
1906+ (?:INSERT|REPLACE)
1907+ (?:\s+LOW_PRIORITY|DELAYED|HIGH_PRIORITY|IGNORE)?
1908+ (?:\s\w+)*\s+\S+\s+VALUES\s*\(.*?\)
1909+ )
1910+ \s*,\s*\(.*?(ON\s+DUPLICATE|\Z)}
1911+ {$1 /*... omitted ...*/$2}xsi;
1912+
1913+ return $query unless $query =~ m/IN\s*\(\s*(?!select)/i;
1914+
1915+ my $last_length = 0;
1916+ my $query_length = length($query);
1917+ while (
1918+ $length > 0
1919+ && $query_length > $length
1920+ && $query_length < ( $last_length || $query_length + 1 )
1921+ ) {
1922+ $last_length = $query_length;
1923+ $query =~ s{
1924+ (\bIN\s*\() # The opening of an IN list
1925+ ([^\)]+) # Contents of the list, assuming no item contains paren
1926+ (?=\)) # Close of the list
1927+ }
1928+ {
1929+ $1 . __shorten($2)
1930+ }gexsi;
1931+ }
1932+
1933+ return $query;
1934+}
1935+
1936+sub __shorten {
1937+ my ( $snippet ) = @_;
1938+ my @vals = split(/,/, $snippet);
1939+ return $snippet unless @vals > 20;
1940+ my @keep = splice(@vals, 0, 20); # Remove and save the first 20 items
1941+ return
1942+ join(',', @keep)
1943+ . "/*... omitted "
1944+ . scalar(@vals)
1945+ . " items ...*/";
1946+}
1947+
1948+sub fingerprint {
1949+ my ( $self, $query ) = @_;
1950+
1951+ $query =~ m#\ASELECT /\*!40001 SQL_NO_CACHE \*/ \* FROM `# # mysqldump query
1952+ && return 'mysqldump';
1953+ $query =~ m#/\*\w+\.\w+:[0-9]/[0-9]\*/# # pt-table-checksum, etc query
1954+ && return 'percona-toolkit';
1955+ $query =~ m/\Aadministrator command: /
1956+ && return $query;
1957+ $query =~ m/\A\s*(call\s+\S+)\(/i
1958+ && return lc($1); # Warning! $1 used, be careful.
1959+ if ( my ($beginning) = $query =~ m/\A((?:INSERT|REPLACE)(?: IGNORE)?\s+INTO.+?VALUES\s*\(.*?\))\s*,\s*\(/is ) {
1960+ $query = $beginning; # Shorten multi-value INSERT statements ASAP
1961+ }
1962+
1963+ $query =~ s/$olc_re//go;
1964+ $query =~ s/$mlc_re//go;
1965+ $query =~ s/\Ause \S+\Z/use ?/i # Abstract the DB in USE
1966+ && return $query;
1967+
1968+ $query =~ s/\\["']//g; # quoted strings
1969+ $query =~ s/".*?"/?/sg; # quoted strings
1970+ $query =~ s/'.*?'/?/sg; # quoted strings
1971+
1972+ if ( $self->{match_md5_checksums} ) {
1973+ $query =~ s/([._-])[a-f0-9]{32}/$1?/g;
1974+ }
1975+
1976+ if ( !$self->{match_embedded_numbers} ) {
1977+ $query =~ s/[0-9+-][0-9a-f.xb+-]*/?/g;
1978+ }
1979+ else {
1980+ $query =~ s/\b[0-9+-][0-9a-f.xb+-]*/?/g;
1981+ }
1982+
1983+ if ( $self->{match_md5_checksums} ) {
1984+ $query =~ s/[xb+-]\?/?/g;
1985+ }
1986+ else {
1987+ $query =~ s/[xb.+-]\?/?/g;
1988+ }
1989+
1990+ $query =~ s/\A\s+//; # Chop off leading whitespace
1991+ chomp $query; # Kill trailing whitespace
1992+ $query =~ tr[ \n\t\r\f][ ]s; # Collapse whitespace
1993+ $query = lc $query;
1994+ $query =~ s/\bnull\b/?/g; # Get rid of NULLs
1995+ $query =~ s{ # Collapse IN and VALUES lists
1996+ \b(in|values?)(?:[\s,]*\([\s?,]*\))+
1997+ }
1998+ {$1(?+)}gx;
1999+ $query =~ s{ # Collapse UNION
2000+ \b(select\s.*?)(?:(\sunion(?:\sall)?)\s\1)+
2001+ }
2002+ {$1 /*repeat$2*/}xg;
2003+ $query =~ s/\blimit \?(?:, ?\?| offset \?)?/limit ?/; # LIMIT
2004+
2005+ if ( $query =~ m/\bORDER BY /gi ) { # Find, anchor on ORDER BY clause
2006+ 1 while $query =~ s/\G(.+?)\s+ASC/$1/gi && pos $query;
2007+ }
2008+
2009+ return $query;
2010+}
2011+
2012+sub distill_verbs {
2013+ my ( $self, $query ) = @_;
2014+
2015+ $query =~ m/\A\s*call\s+(\S+)\(/i && return "CALL $1";
2016+ $query =~ m/\A\s*use\s+/ && return "USE";
2017+ $query =~ m/\A\s*UNLOCK TABLES/i && return "UNLOCK";
2018+ $query =~ m/\A\s*xa\s+(\S+)/i && return "XA_$1";
2019+
2020+ if ( $query =~ m/\Aadministrator command:/ ) {
2021+ $query =~ s/administrator command:/ADMIN/;
2022+ $query = uc $query;
2023+ return $query;
2024+ }
2025+
2026+ $query = $self->strip_comments($query);
2027+
2028+ if ( $query =~ m/\A\s*SHOW\s+/i ) {
2029+ PTDEBUG && _d($query);
2030+
2031+ $query = uc $query;
2032+ $query =~ s/\s+(?:GLOBAL|SESSION|FULL|STORAGE|ENGINE)\b/ /g;
2033+ $query =~ s/\s+COUNT[^)]+\)//g;
2034+
2035+ $query =~ s/\s+(?:FOR|FROM|LIKE|WHERE|LIMIT|IN)\b.+//ms;
2036+
2037+ $query =~ s/\A(SHOW(?:\s+\S+){1,2}).*\Z/$1/s;
2038+ $query =~ s/\s+/ /g;
2039+ PTDEBUG && _d($query);
2040+ return $query;
2041+ }
2042+
2043+ eval $QueryParser::data_def_stmts;
2044+ eval $QueryParser::tbl_ident;
2045+ my ( $dds ) = $query =~ /^\s*($QueryParser::data_def_stmts)\b/i;
2046+ if ( $dds) {
2047+ my ( $obj ) = $query =~ m/$dds.+(DATABASE|TABLE)\b/i;
2048+ $obj = uc $obj if $obj;
2049+ PTDEBUG && _d('Data def statment:', $dds, 'obj:', $obj);
2050+ my ($db_or_tbl)
2051+ = $query =~ m/(?:TABLE|DATABASE)\s+($QueryParser::tbl_ident)(\s+.*)?/i;
2052+ PTDEBUG && _d('Matches db or table:', $db_or_tbl);
2053+ return uc($dds . ($obj ? " $obj" : '')), $db_or_tbl;
2054+ }
2055+
2056+ my @verbs = $query =~ m/\b($verbs)\b/gio;
2057+ @verbs = do {
2058+ my $last = '';
2059+ grep { my $pass = $_ ne $last; $last = $_; $pass } map { uc } @verbs;
2060+ };
2061+
2062+ if ( ($verbs[0] || '') eq 'SELECT' && @verbs > 1 ) {
2063+ PTDEBUG && _d("False-positive verbs after SELECT:", @verbs[1..$#verbs]);
2064+ my $union = grep { $_ eq 'UNION' } @verbs;
2065+ @verbs = $union ? qw(SELECT UNION) : qw(SELECT);
2066+ }
2067+
2068+ my $verb_str = join(q{ }, @verbs);
2069+ return $verb_str;
2070+}
2071+
2072+sub __distill_tables {
2073+ my ( $self, $query, $table, %args ) = @_;
2074+ my $qp = $args{QueryParser} || $self->{QueryParser};
2075+ die "I need a QueryParser argument" unless $qp;
2076+
2077+ my @tables = map {
2078+ $_ =~ s/`//g;
2079+ $_ =~ s/(_?)[0-9]+/$1?/g;
2080+ $_;
2081+ } grep { defined $_ } $qp->get_tables($query);
2082+
2083+ push @tables, $table if $table;
2084+
2085+ @tables = do {
2086+ my $last = '';
2087+ grep { my $pass = $_ ne $last; $last = $_; $pass } @tables;
2088+ };
2089+
2090+ return @tables;
2091+}
2092+
2093+sub distill {
2094+ my ( $self, $query, %args ) = @_;
2095+
2096+ if ( $args{generic} ) {
2097+ my ($cmd, $arg) = $query =~ m/^(\S+)\s+(\S+)/;
2098+ return '' unless $cmd;
2099+ $query = (uc $cmd) . ($arg ? " $arg" : '');
2100+ }
2101+ else {
2102+ my ($verbs, $table) = $self->distill_verbs($query, %args);
2103+
2104+ if ( $verbs && $verbs =~ m/^SHOW/ ) {
2105+ my %alias_for = qw(
2106+ SCHEMA DATABASE
2107+ KEYS INDEX
2108+ INDEXES INDEX
2109+ );
2110+ map { $verbs =~ s/$_/$alias_for{$_}/ } keys %alias_for;
2111+ $query = $verbs;
2112+ }
2113+ else {
2114+ my @tables = $self->__distill_tables($query, $table, %args);
2115+ $query = join(q{ }, $verbs, @tables);
2116+ }
2117+ }
2118+
2119+ if ( $args{trf} ) {
2120+ $query = $args{trf}->($query, %args);
2121+ }
2122+
2123+ return $query;
2124+}
2125+
2126+sub convert_to_select {
2127+ my ( $self, $query ) = @_;
2128+ return unless $query;
2129+
2130+ return if $query =~ m/=\s*\(\s*SELECT /i;
2131+
2132+ $query =~ s{
2133+ \A.*?
2134+ update(?:\s+(?:low_priority|ignore))?\s+(.*?)
2135+ \s+set\b(.*?)
2136+ (?:\s*where\b(.*?))?
2137+ (limit\s*[0-9]+(?:\s*,\s*[0-9]+)?)?
2138+ \Z
2139+ }
2140+ {__update_to_select($1, $2, $3, $4)}exsi
2141+ || $query =~ s{
2142+ \A.*?
2143+ (?:insert(?:\s+ignore)?|replace)\s+
2144+ .*?\binto\b(.*?)\(([^\)]+)\)\s*
2145+ values?\s*(\(.*?\))\s*
2146+ (?:\blimit\b|on\s+duplicate\s+key.*)?\s*
2147+ \Z
2148+ }
2149+ {__insert_to_select($1, $2, $3)}exsi
2150+ || $query =~ s{
2151+ \A.*?
2152+ (?:insert(?:\s+ignore)?|replace)\s+
2153+ (?:.*?\binto)\b(.*?)\s*
2154+ set\s+(.*?)\s*
2155+ (?:\blimit\b|on\s+duplicate\s+key.*)?\s*
2156+ \Z
2157+ }
2158+ {__insert_to_select_with_set($1, $2)}exsi
2159+ || $query =~ s{
2160+ \A.*?
2161+ delete\s+(.*?)
2162+ \bfrom\b(.*)
2163+ \Z
2164+ }
2165+ {__delete_to_select($1, $2)}exsi;
2166+ $query =~ s/\s*on\s+duplicate\s+key\s+update.*\Z//si;
2167+ $query =~ s/\A.*?(?=\bSELECT\s*\b)//ism;
2168+ return $query;
2169+}
2170+
2171+sub convert_select_list {
2172+ my ( $self, $query ) = @_;
2173+ $query =~ s{
2174+ \A\s*select(.*?)\bfrom\b
2175+ }
2176+ {$1 =~ m/\*/ ? "select 1 from" : "select isnull(coalesce($1)) from"}exi;
2177+ return $query;
2178+}
2179+
2180+sub __delete_to_select {
2181+ my ( $delete, $join ) = @_;
2182+ if ( $join =~ m/\bjoin\b/ ) {
2183+ return "select 1 from $join";
2184+ }
2185+ return "select * from $join";
2186+}
2187+
2188+sub __insert_to_select {
2189+ my ( $tbl, $cols, $vals ) = @_;
2190+ PTDEBUG && _d('Args:', @_);
2191+ my @cols = split(/,/, $cols);
2192+ PTDEBUG && _d('Cols:', @cols);
2193+ $vals =~ s/^\(|\)$//g; # Strip leading/trailing parens
2194+ my @vals = $vals =~ m/($quote_re|[^,]*${bal}[^,]*|[^,]+)/g;
2195+ PTDEBUG && _d('Vals:', @vals);
2196+ if ( @cols == @vals ) {
2197+ return "select * from $tbl where "
2198+ . join(' and ', map { "$cols[$_]=$vals[$_]" } (0..$#cols));
2199+ }
2200+ else {
2201+ return "select * from $tbl limit 1";
2202+ }
2203+}
2204+
2205+sub __insert_to_select_with_set {
2206+ my ( $from, $set ) = @_;
2207+ $set =~ s/,/ and /g;
2208+ return "select * from $from where $set ";
2209+}
2210+
2211+sub __update_to_select {
2212+ my ( $from, $set, $where, $limit ) = @_;
2213+ return "select $set from $from "
2214+ . ( $where ? "where $where" : '' )
2215+ . ( $limit ? " $limit " : '' );
2216+}
2217+
2218+sub wrap_in_derived {
2219+ my ( $self, $query ) = @_;
2220+ return unless $query;
2221+ return $query =~ m/\A\s*select/i
2222+ ? "select 1 from ($query) as x limit 1"
2223+ : $query;
2224+}
2225+
2226+sub _d {
2227+ my ($package, undef, $line) = caller 0;
2228+ @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; }
2229+ map { defined $_ ? $_ : 'undef' }
2230+ @_;
2231+ print STDERR "# $package:$line $PID ", join(' ', @_), "\n";
2232+}
2233+
2234+1;
2235+}
2236+# ###########################################################################
2237+# End QueryRewriter package
2238+# ###########################################################################
2239+
2240+# ###########################################################################
2241+# This is a combination of modules and programs in one -- a runnable module.
2242+# http://www.perl.com/pub/a/2006/07/13/lightning-articles.html?page=last
2243+# Or, look it up in the Camel book on pages 642 and 643 in the 3rd edition.
2244+#
2245+# Check at the end of this package for the call to main() which actually runs
2246+# the program.
2247+# ###########################################################################
2248+package pt_fingerprint;
2249+
2250+use English qw(-no_match_vars);
2251+use Data::Dumper;
2252+$Data::Dumper::Indent = 1;
2253+$OUTPUT_AUTOFLUSH = 1;
2254+
2255+use constant MKDEBUG => $ENV{MKDEBUG} || 0;
2256+
2257+sub main {
2258+ @ARGV = @_; # set global ARGV for this package
2259+
2260+ # ##########################################################################
2261+ # Get configuration information.
2262+ # ##########################################################################
2263+ my $o = new OptionParser();
2264+ $o->get_specs();
2265+ $o->get_opts();
2266+ $o->usage_or_errors();
2267+
2268+ my $qp = new QueryParser();
2269+ my $qr = new QueryRewriter(
2270+ QueryParser => $qp,
2271+ match_md5_checksums => $o->get('match-md5-checksums'),
2272+ match_embedded_numbers => $o->get('match-embedded-numbers'),
2273+ );
2274+
2275+ if ( $o->got('query') ) {
2276+ print $qr->fingerprint($o->get('query')), "\n";
2277+ }
2278+ else {
2279+ local $INPUT_RECORD_SEPARATOR = ";\n";
2280+ while ( <> ) {
2281+ my $query = $_;
2282+ chomp $query;
2283+ $query =~ s/^#.+$//mg;
2284+ $query =~ s/^\s+//;
2285+ next unless $query =~ m/^\w/;
2286+ print $qr->fingerprint($query), "\n";
2287+ }
2288+ }
2289+}
2290+
2291+# ############################################################################
2292+# Run the program.
2293+# ############################################################################
2294+if ( !caller ) { exit main(@ARGV); }
2295+
2296+1; # Because this is a module as well as a script.
2297+
2298+# #############################################################################
2299+# Documentation.
2300+# #############################################################################
2301+
2302+=pod
2303+
2304+=head1 NAME
2305+
2306+pt-fingerprint - Convert queries into fingerprints.
2307+
2308+=head1 SYNOPSIS
2309+
2310+Usage: pt-fingerprint [OPTIONS] [FILES]
2311+
2312+pt-fingerprint converts queries into fingerprints. With the --query
2313+option, converts the option's value into a fingerprint. With no options, treats
2314+command-line arguments as FILEs and reads and converts semicolon-separated
2315+queries from the FILEs. When FILE is -, it read standard input.
2316+
2317+Convert a single query:
2318+
2319+ pt-fingerprint --query "select a, b, c from users where id = 500"
2320+
2321+Convert a file full of queries:
2322+
2323+ pt-fingerprint /path/to/file.txt
2324+
2325+=head1 RISKS
2326+
2327+The following section is included to inform users about the potential risks,
2328+whether known or unknown, of using this tool. The two main categories of risks
2329+are those created by the nature of the tool (e.g. read-only tools vs. read-write
2330+tools) and those created by bugs.
2331+
2332+The pt-fingerprint tool simply reads data and transforms it, so risks are
2333+minimal.
2334+
2335+See also L<"BUGS"> for more information on filing bugs and getting help.
2336+
2337+=head1 DESCRIPTION
2338+
2339+A query fingerprint is the abstracted form of a query, which makes it possible
2340+to group similar queries together. Abstracting a query removes literal values,
2341+normalizes whitespace, and so on. For example, consider these two queries:
2342+
2343+ SELECT name, password FROM user WHERE id='12823';
2344+ select name, password from user
2345+ where id=5;
2346+
2347+Both of those queries will fingerprint to
2348+
2349+ select name, password from user where id=?
2350+
2351+Once the query's fingerprint is known, we can then talk about a query as though
2352+it represents all similar queries.
2353+
2354+Query fingerprinting accommodates a great many special cases, which have proven
2355+necessary in the real world. For example, an IN list with 5 literals is really
2356+equivalent to one with 4 literals, so lists of literals are collapsed to a
2357+single one. If you want to understand more about how and why all of these cases
2358+are handled, please review the test cases in the Subversion repository. If you
2359+find something that is not fingerprinted properly, please submit a bug report
2360+with a reproducible test case. Here is a list of transformations during
2361+fingerprinting, which might not be exhaustive:
2362+
2363+=over
2364+
2365+=item *
2366+
2367+Group all SELECT queries from mysqldump together, even if they are against
2368+different tables. Ditto for all of pt-table-checksum's checksum queries.
2369+
2370+=item *
2371+
2372+Shorten multi-value INSERT statements to a single VALUES() list.
2373+
2374+=item *
2375+
2376+Strip comments.
2377+
2378+=item *
2379+
2380+Abstract the databases in USE statements, so all USE statements are grouped
2381+together.
2382+
2383+=item *
2384+
2385+Replace all literals, such as quoted strings. For efficiency, the code that
2386+replaces literal numbers is somewhat non-selective, and might replace some
2387+things as numbers when they really are not. Hexadecimal literals are also
2388+replaced. NULL is treated as a literal. Numbers embedded in identifiers are
2389+also replaced, so tables named similarly will be fingerprinted to the same
2390+values (e.g. users_2009 and users_2010 will fingerprint identically).
2391+
2392+=item *
2393+
2394+Collapse all whitespace into a single space.
2395+
2396+=item *
2397+
2398+Lowercase the entire query.
2399+
2400+=item *
2401+
2402+Replace all literals inside of IN() and VALUES() lists with a single
2403+placeholder, regardless of cardinality.
2404+
2405+=item *
2406+
2407+Collapse multiple identical UNION queries into a single one.
2408+
2409+=back
2410+
2411+=head1 OPTIONS
2412+
2413+This tool accepts additional command-line arguments. Refer to the
2414+L<"SYNOPSIS"> and usage information for details.
2415+
2416+=over
2417+
2418+=item --config
2419+
2420+type: Array
2421+
2422+Read this comma-separated list of config files; if specified, this must be the
2423+first option on the command line.
2424+
2425+=item --help
2426+
2427+Show help and exit.
2428+
2429+=item --match-embedded-numbers
2430+
2431+Match numbers embedded in words and replace as single values. This option
2432+causes the tool to be more careful about matching numbers so that words
2433+with numbers, like C<catch22> are matched and replaced as a single C<?>
2434+placeholder. Otherwise the default number matching pattern will replace
2435+C<catch22> as C<catch?>.
2436+
2437+This is helpful if database or table names contain numbers.
2438+
2439+=item --match-md5-checksums
2440+
2441+Match MD5 checksums and replace as single values. This option causes
2442+the tool to be more careful about matching numbers so that MD5 checksums
2443+like C<fbc5e685a5d3d45aa1d0347fdb7c4d35> are matched and replaced as a
2444+single C<?> placeholder. Otherwise, the default number matching pattern will
2445+replace C<fbc5e685a5d3d45aa1d0347fdb7c4d35> as C<fbc?>.
2446+
2447+=item --query
2448+
2449+type: string
2450+
2451+The query to convert into a fingerprint.
2452+
2453+=item --version
2454+
2455+Show version and exit.
2456+
2457+=back
2458+
2459+=head1 ENVIRONMENT
2460+
2461+The environment variable C<PTDEBUG> enables verbose debugging output to STDERR.
2462+To enable debugging and capture all output to a file, run the tool like:
2463+
2464+ PTDEBUG=1 pt-fingerprint ... > FILE 2>&1
2465+
2466+Be careful: debugging output is voluminous and can generate several megabytes
2467+of output.
2468+
2469+=head1 SYSTEM REQUIREMENTS
2470+
2471+You need Perl, DBI, DBD::mysql, and some core packages that ought to be
2472+installed in any reasonably new version of Perl.
2473+
2474+=head1 BUGS
2475+
2476+For a list of known bugs, see L<http://www.percona.com/bugs/pt-fingerprint>.
2477+
2478+Please report bugs at L<https://bugs.launchpad.net/percona-toolkit>.
2479+Include the following information in your bug report:
2480+
2481+=over
2482+
2483+=item * Complete command-line used to run the tool
2484+
2485+=item * Tool L<"--version">
2486+
2487+=item * MySQL version of all servers involved
2488+
2489+=item * Output from the tool including STDERR
2490+
2491+=item * Input files (log/dump/config files, etc.)
2492+
2493+=back
2494+
2495+If possible, include debugging output by running the tool with C<PTDEBUG>;
2496+see L<"ENVIRONMENT">.
2497+
2498+=head1 DOWNLOADING
2499+
2500+Visit L<http://www.percona.com/software/percona-toolkit/> to download the
2501+latest release of Percona Toolkit. Or, get the latest release from the
2502+command line:
2503+
2504+ wget percona.com/get/percona-toolkit.tar.gz
2505+
2506+ wget percona.com/get/percona-toolkit.rpm
2507+
2508+ wget percona.com/get/percona-toolkit.deb
2509+
2510+You can also get individual tools from the latest release:
2511+
2512+ wget percona.com/get/TOOL
2513+
2514+Replace C<TOOL> with the name of any tool.
2515+
2516+=head1 AUTHORS
2517+
2518+Baron Schwartz and Daniel Nichter
2519+
2520+=head1 ABOUT PERCONA TOOLKIT
2521+
2522+This tool is part of Percona Toolkit, a collection of advanced command-line
2523+tools developed by Percona for MySQL support and consulting. Percona Toolkit
2524+was forked from two projects in June, 2011: Maatkit and Aspersa. Those
2525+projects were created by Baron Schwartz and developed primarily by him and
2526+Daniel Nichter, both of whom are employed by Percona. Visit
2527+L<http://www.percona.com/software/> for more software developed by Percona.
2528+
2529+=head1 COPYRIGHT, LICENSE, AND WARRANTY
2530+
2531+This program is copyright 2011-2012 Percona Inc.
2532+Feedback and improvements are welcome.
2533+
2534+THIS PROGRAM IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
2535+WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
2536+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
2537+
2538+This program is free software; you can redistribute it and/or modify it under
2539+the terms of the GNU General Public License as published by the Free Software
2540+Foundation, version 2; OR the Perl Artistic License. On UNIX and similar
2541+systems, you can issue `man perlgpl' or `man perlartistic' to read these
2542+licenses.
2543+
2544+You should have received a copy of the GNU General Public License along with
2545+this program; if not, write to the Free Software Foundation, Inc., 59 Temple
2546+Place, Suite 330, Boston, MA 02111-1307 USA.
2547+
2548+=head1 VERSION
2549+
2550+pt-fingerprint 2.0.0
2551+
2552+=cut
2553
2554=== modified file 'bin/pt-index-usage'
2555--- bin/pt-index-usage 2012-03-07 23:41:54 +0000
2556+++ bin/pt-index-usage 2012-03-31 16:07:24 +0000
2557@@ -2669,19 +2669,58 @@
2558 return bless $self, $class;
2559 }
2560
2561+sub get_create_table {
2562+ my ( $self, $dbh, $db, $tbl ) = @_;
2563+ die "I need a dbh parameter" unless $dbh;
2564+ die "I need a db parameter" unless $db;
2565+ die "I need a tbl parameter" unless $tbl;
2566+ my $q = $self->{Quoter};
2567+
2568+ my $new_sql_mode
2569+ = '/*!40101 SET @OLD_SQL_MODE := @@SQL_MODE, '
2570+ . q{@@SQL_MODE := REPLACE(REPLACE(@@SQL_MODE, 'ANSI_QUOTES', ''), ',,', ','), }
2571+ . '@OLD_QUOTE := @@SQL_QUOTE_SHOW_CREATE, '
2572+ . '@@SQL_QUOTE_SHOW_CREATE := 1 */';
2573+
2574+ my $old_sql_mode = '/*!40101 SET @@SQL_MODE := @OLD_SQL_MODE, '
2575+ . '@@SQL_QUOTE_SHOW_CREATE := @OLD_QUOTE */';
2576+
2577+ PTDEBUG && _d($new_sql_mode);
2578+ eval { $dbh->do($new_sql_mode); };
2579+ PTDEBUG && $EVAL_ERROR && _d($EVAL_ERROR);
2580+
2581+ my $use_sql = 'USE ' . $q->quote($db);
2582+ PTDEBUG && _d($dbh, $use_sql);
2583+ $dbh->do($use_sql);
2584+
2585+ my $show_sql = "SHOW CREATE TABLE " . $q->quote($db, $tbl);
2586+ PTDEBUG && _d($show_sql);
2587+ my $href;
2588+ eval { $href = $dbh->selectrow_hashref($show_sql); };
2589+ if ( $EVAL_ERROR ) {
2590+ PTDEBUG && _d($EVAL_ERROR);
2591+
2592+ PTDEBUG && _d($old_sql_mode);
2593+ $dbh->do($old_sql_mode);
2594+
2595+ return;
2596+ }
2597+
2598+ PTDEBUG && _d($old_sql_mode);
2599+ $dbh->do($old_sql_mode);
2600+
2601+ my ($key) = grep { m/create (?:table|view)/i } keys %$href;
2602+ if ( !$key ) {
2603+ die "Error: no 'Create Table' or 'Create View' in result set from "
2604+ . "$show_sql: " . Dumper($href);
2605+ }
2606+
2607+ return $href->{$key};
2608+}
2609+
2610 sub parse {
2611 my ( $self, $ddl, $opts ) = @_;
2612 return unless $ddl;
2613- if ( ref $ddl eq 'ARRAY' ) {
2614- if ( lc $ddl->[0] eq 'table' ) {
2615- $ddl = $ddl->[1];
2616- }
2617- else {
2618- return {
2619- engine => 'VIEW',
2620- };
2621- }
2622- }
2623
2624 if ( $ddl !~ m/CREATE (?:TEMPORARY )?TABLE `/ ) {
2625 die "Cannot parse table definition; is ANSI quoting "
2626@@ -2988,41 +3027,31 @@
2627 return $ddl;
2628 }
2629
2630-sub remove_secondary_indexes {
2631- my ( $self, $ddl ) = @_;
2632- my $sec_indexes_ddl;
2633- my $tbl_struct = $self->parse($ddl);
2634-
2635- if ( ($tbl_struct->{engine} || '') =~ m/InnoDB/i ) {
2636- my $clustered_key = $tbl_struct->{clustered_key};
2637- $clustered_key ||= '';
2638-
2639- my @sec_indexes = map {
2640- my $key_def = $_->{ddl};
2641- $key_def =~ s/([\(\)])/\\$1/g;
2642- $ddl =~ s/\s+$key_def//i;
2643-
2644- my $key_ddl = "ADD $_->{ddl}";
2645- $key_ddl .= ',' unless $key_ddl =~ m/,$/;
2646- $key_ddl;
2647- }
2648- grep { $_->{name} ne $clustered_key }
2649- values %{$tbl_struct->{keys}};
2650- PTDEBUG && _d('Secondary indexes:', Dumper(\@sec_indexes));
2651-
2652- if ( @sec_indexes ) {
2653- $sec_indexes_ddl = join(' ', @sec_indexes);
2654- $sec_indexes_ddl =~ s/,$//;
2655- }
2656-
2657- $ddl =~ s/,(\n\) )/$1/s;
2658- }
2659- else {
2660- PTDEBUG && _d('Not removing secondary indexes from',
2661- $tbl_struct->{engine}, 'table');
2662- }
2663-
2664- return $ddl, $sec_indexes_ddl, $tbl_struct;
2665+sub get_table_status {
2666+ my ( $self, $dbh, $db, $like ) = @_;
2667+ my $q = $self->{Quoter};
2668+ my $sql = "SHOW TABLE STATUS FROM " . $q->quote($db);
2669+ my @params;
2670+ if ( $like ) {
2671+ $sql .= ' LIKE ?';
2672+ push @params, $like;
2673+ }
2674+ PTDEBUG && _d($sql, @params);
2675+ my $sth = $dbh->prepare($sql);
2676+ eval { $sth->execute(@params); };
2677+ if ($EVAL_ERROR) {
2678+ PTDEBUG && _d($EVAL_ERROR);
2679+ return;
2680+ }
2681+ my @tables = @{$sth->fetchall_arrayref({})};
2682+ @tables = map {
2683+ my %tbl; # Make a copy with lowercased keys
2684+ @tbl{ map { lc $_ } keys %$_ } = values %$_;
2685+ $tbl{engine} ||= $tbl{type} || $tbl{comment};
2686+ delete $tbl{type};
2687+ \%tbl;
2688+ } @tables;
2689+ return @tables;
2690 }
2691
2692 sub _d {
2693@@ -3912,7 +3941,7 @@
2694
2695 sub new {
2696 my ( $class, %args ) = @_;
2697- my @required_args = qw(OptionParser Quoter);
2698+ my @required_args = qw(OptionParser TableParser Quoter);
2699 foreach my $arg ( @required_args ) {
2700 die "I need a $arg argument" unless $args{$arg};
2701 }
2702@@ -3921,8 +3950,19 @@
2703 die "I need either a dbh or file_itr argument"
2704 if (!$dbh && !$file_itr) || ($dbh && $file_itr);
2705
2706+ my %resume;
2707+ if ( my $table = $args{resume} ) {
2708+ PTDEBUG && _d('Will resume from or after', $table);
2709+ my ($db, $tbl) = $args{Quoter}->split_unquote($table);
2710+ die "Resume table must be database-qualified: $table"
2711+ unless $db && $tbl;
2712+ $resume{db} = $db;
2713+ $resume{tbl} = $tbl;
2714+ }
2715+
2716 my $self = {
2717 %args,
2718+ resume => \%resume,
2719 filters => _make_filters(%args),
2720 };
2721
2722@@ -3983,9 +4023,19 @@
2723 return \%filters;
2724 }
2725
2726-sub next_schema_object {
2727+sub next {
2728 my ( $self ) = @_;
2729
2730+ if ( !$self->{initialized} ) {
2731+ $self->{initialized} = 1;
2732+ if ( $self->{resume}->{tbl}
2733+ && !$self->table_is_allowed(@{$self->{resume}}{qw(db tbl)}) ) {
2734+ PTDEBUG && _d('Will resume after',
2735+ join('.', @{$self->{resume}}{qw(db tbl)}));
2736+ $self->{resume}->{after} = 1;
2737+ }
2738+ }
2739+
2740 my $schema_obj;
2741 if ( $self->{file_itr} ) {
2742 $schema_obj= $self->_iterate_files();
2743@@ -3995,24 +4045,18 @@
2744 }
2745
2746 if ( $schema_obj ) {
2747- if ( $schema_obj->{ddl} && $self->{TableParser} ) {
2748- $schema_obj->{tbl_struct}
2749- = $self->{TableParser}->parse($schema_obj->{ddl});
2750- }
2751-
2752- delete $schema_obj->{ddl} unless $self->{keep_ddl};
2753-
2754 if ( my $schema = $self->{Schema} ) {
2755 $schema->add_schema_object($schema_obj);
2756 }
2757+ PTDEBUG && _d('Next schema object:',
2758+ $schema_obj->{db}, $schema_obj->{tbl});
2759 }
2760
2761- PTDEBUG && _d('Next schema object:', $schema_obj->{db}, $schema_obj->{tbl});
2762 return $schema_obj;
2763 }
2764
2765 sub _iterate_files {
2766- my ( $self ) = @_;
2767+ my ( $self ) = @_;
2768
2769 if ( !$self->{fh} ) {
2770 my ($fh, $file) = $self->{file_itr}->();
2771@@ -4033,7 +4077,8 @@
2772 my $db = $1; # XXX
2773 $db =~ s/^`//; # strip leading `
2774 $db =~ s/`$//; # and trailing `
2775- if ( $self->database_is_allowed($db) ) {
2776+ if ( $self->database_is_allowed($db)
2777+ && $self->_resume_from_database($db) ) {
2778 $self->{db} = $db;
2779 }
2780 }
2781@@ -4046,21 +4091,22 @@
2782 my ($tbl) = $chunk =~ m/$tbl_name/;
2783 $tbl =~ s/^\s*`//;
2784 $tbl =~ s/`\s*$//;
2785- if ( $self->table_is_allowed($self->{db}, $tbl) ) {
2786+ if ( $self->_resume_from_table($tbl)
2787+ && $self->table_is_allowed($self->{db}, $tbl) ) {
2788 my ($ddl) = $chunk =~ m/^(?:$open_comment)?(CREATE TABLE.+?;)$/ms;
2789 if ( !$ddl ) {
2790 warn "Failed to parse CREATE TABLE from\n" . $chunk;
2791 next CHUNK;
2792 }
2793 $ddl =~ s/ \*\/;\Z/;/; # remove end of version comment
2794-
2795- my ($engine) = $ddl =~ m/\).*?(?:ENGINE|TYPE)=(\w+)/;
2796-
2797- if ( !$engine || $self->engine_is_allowed($engine) ) {
2798+ my $tbl_struct = $self->{TableParser}->parse($ddl);
2799+ if ( $self->engine_is_allowed($tbl_struct->{engine}) ) {
2800 return {
2801- db => $self->{db},
2802- tbl => $tbl,
2803- ddl => $ddl,
2804+ db => $self->{db},
2805+ tbl => $tbl,
2806+ name => $self->{Quoter}->quote($self->{db}, $tbl),
2807+ ddl => $ddl,
2808+ tbl_struct => $tbl_struct,
2809 };
2810 }
2811 }
2812@@ -4077,6 +4123,7 @@
2813 sub _iterate_dbh {
2814 my ( $self ) = @_;
2815 my $q = $self->{Quoter};
2816+ my $tp = $self->{TableParser};
2817 my $dbh = $self->{dbh};
2818 PTDEBUG && _d('Getting next schema object from dbh', $dbh);
2819
2820@@ -4090,7 +4137,9 @@
2821 }
2822
2823 if ( !$self->{db} ) {
2824- $self->{db} = shift @{$self->{dbs}};
2825+ do {
2826+ $self->{db} = shift @{$self->{dbs}};
2827+ } until $self->_resume_from_database($self->{db});
2828 PTDEBUG && _d('Next database:', $self->{db});
2829 return unless $self->{db};
2830 }
2831@@ -4103,8 +4152,9 @@
2832 }
2833 grep {
2834 my ($tbl, $type) = @$_;
2835- $self->table_is_allowed($self->{db}, $tbl)
2836- && (!$type || ($type ne 'VIEW'));
2837+ (!$type || ($type ne 'VIEW'))
2838+ && $self->_resume_from_table($tbl)
2839+ && $self->table_is_allowed($self->{db}, $tbl);
2840 }
2841 @{$dbh->selectall_arrayref($sql)};
2842 PTDEBUG && _d('Found', scalar @tbls, 'tables in database', $self->{db});
2843@@ -4112,27 +4162,15 @@
2844 }
2845
2846 while ( my $tbl = shift @{$self->{tbls}} ) {
2847- my $engine;
2848- if ( $self->{filters}->{'engines'}
2849- || $self->{filters}->{'ignore-engines'} ) {
2850- my $sql = "SHOW TABLE STATUS FROM " . $q->quote($self->{db})
2851- . " LIKE \'$tbl\'";
2852- PTDEBUG && _d($sql);
2853- $engine = $dbh->selectrow_hashref($sql)->{engine};
2854- PTDEBUG && _d($tbl, 'uses', $engine, 'engine');
2855- }
2856-
2857-
2858- if ( !$engine || $self->engine_is_allowed($engine) ) {
2859- my $ddl;
2860- if ( my $du = $self->{MySQLDump} ) {
2861- $ddl = $du->get_create_table($dbh, $q, $self->{db}, $tbl)->[1];
2862- }
2863-
2864+ my $ddl = $tp->get_create_table($dbh, $self->{db}, $tbl);
2865+ my $tbl_struct = $tp->parse($ddl);
2866+ if ( $self->engine_is_allowed($tbl_struct->{engine}) ) {
2867 return {
2868- db => $self->{db},
2869- tbl => $tbl,
2870- ddl => $ddl,
2871+ db => $self->{db},
2872+ tbl => $tbl,
2873+ name => $q->quote($self->{db}, $tbl),
2874+ ddl => $ddl,
2875+ tbl_struct => $tbl_struct,
2876 };
2877 }
2878 }
2879@@ -4193,6 +4231,10 @@
2880
2881 my $filter = $self->{filters};
2882
2883+ if ( $db eq 'mysql' && ($tbl eq 'general_log' || $tbl eq 'slow_log') ) {
2884+ return 0;
2885+ }
2886+
2887 if ( $filter->{'ignore-tables'}->{$tbl}
2888 && ($filter->{'ignore-tables'}->{$tbl} eq '*'
2889 || $filter->{'ignore-tables'}->{$tbl} eq $db) ) {
2890@@ -4232,7 +4274,11 @@
2891
2892 sub engine_is_allowed {
2893 my ( $self, $engine ) = @_;
2894- die "I need an engine argument" unless $engine;
2895+
2896+ if ( !$engine ) {
2897+ PTDEBUG && _d('No engine specified; allowing the table');
2898+ return 1;
2899+ }
2900
2901 $engine = lc $engine;
2902
2903@@ -4252,6 +4298,40 @@
2904 return 1;
2905 }
2906
2907+sub _resume_from_database {
2908+ my ($self, $db) = @_;
2909+
2910+ return 1 unless $self->{resume}->{db};
2911+
2912+ if ( $db eq $self->{resume}->{db} ) {
2913+ PTDEBUG && _d('At resume db', $db);
2914+ delete $self->{resume}->{db};
2915+ return 1;
2916+ }
2917+
2918+ return 0;
2919+}
2920+
2921+sub _resume_from_table {
2922+ my ($self, $tbl) = @_;
2923+
2924+ return 1 unless $self->{resume}->{tbl};
2925+
2926+ if ( $tbl eq $self->{resume}->{tbl} ) {
2927+ if ( !$self->{resume}->{after} ) {
2928+ PTDEBUG && _d('Resuming from table', $tbl);
2929+ delete $self->{resume}->{tbl};
2930+ return 1;
2931+ }
2932+ else {
2933+ PTDEBUG && _d('Resuming after table', $tbl);
2934+ delete $self->{resume}->{tbl};
2935+ }
2936+ }
2937+
2938+ return 0;
2939+}
2940+
2941 sub _d {
2942 my ($package, undef, $line) = caller 0;
2943 @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; }
2944@@ -5144,13 +5224,11 @@
2945 dbh => $dbh,
2946 OptionParser => $o,
2947 Quoter => $q,
2948- MySQLDump => $du,
2949 TableParser => $tp,
2950 Schema => $schema,
2951- keep_ddl => 1,
2952 );
2953 TALBE:
2954- while ( my $tbl = $schema_itr->next_schema_object() ) {
2955+ while ( my $tbl = $schema_itr->next() ) {
2956 eval {
2957 my ($indexes) = $tp->get_keys($tbl->{ddl}, {version => $version});
2958 $iu->add_indexes(%$tbl, indexes=>$indexes);
2959
2960=== modified file 'bin/pt-table-checksum'
2961--- bin/pt-table-checksum 2012-03-30 16:10:23 +0000
2962+++ bin/pt-table-checksum 2012-03-31 16:07:24 +0000
2963@@ -3941,22 +3941,15 @@
2964
2965 sub get_row_estimate {
2966 my (%args) = @_;
2967- my @required_args = qw(Cxn tbl OptionParser TableParser Quoter);
2968- my ($cxn, $tbl, $o, $tp, $q) = @args{@required_args};
2969+ my @required_args = qw(Cxn tbl);
2970+ my ($cxn, $tbl) = @args{@required_args};
2971
2972- if ( $args{where} ) {
2973- PTDEBUG && _d('WHERE clause, using explain plan for row estimate');
2974- my $table = $q->quote(@{$tbl}{qw(db tbl)});
2975- my $sql = "EXPLAIN SELECT * FROM $table WHERE $args{where}";
2976- PTDEBUG && _d($sql);
2977- my $expl = $cxn->dbh()->selectrow_hashref($sql);
2978- PTDEBUG && _d(Dumper($expl));
2979- return ($expl->{rows} || 0), $expl->{key};
2980- }
2981- else {
2982- PTDEBUG && _d('No WHERE clause, using table status for row estimate');
2983- return $tbl->{tbl_status}->{rows} || 0;
2984- }
2985+ my $sql = "EXPLAIN SELECT * FROM $tbl->{name} "
2986+ . "WHERE " . ($args{where} || '1=1');
2987+ PTDEBUG && _d($sql);
2988+ my $expl = $cxn->dbh()->selectrow_hashref($sql);
2989+ PTDEBUG && _d(Dumper($expl));
2990+ return ($expl->{rows} || 0), $expl->{key};
2991 }
2992
2993 sub _prepare_sths {
2994@@ -4543,7 +4536,7 @@
2995
2996 sub new {
2997 my ( $class, %args ) = @_;
2998- my @required_args = qw(OptionParser Quoter);
2999+ my @required_args = qw(OptionParser TableParser Quoter);
3000 foreach my $arg ( @required_args ) {
3001 die "I need a $arg argument" unless $args{$arg};
3002 }
3003@@ -4647,25 +4640,18 @@
3004 }
3005
3006 if ( $schema_obj ) {
3007- if ( $schema_obj->{ddl} && $self->{TableParser} ) {
3008- $schema_obj->{tbl_struct}
3009- = $self->{TableParser}->parse($schema_obj->{ddl});
3010- }
3011-
3012- delete $schema_obj->{ddl} unless $self->{keep_ddl};
3013- delete $schema_obj->{tbl_status} unless $self->{keep_tbl_status};
3014-
3015 if ( my $schema = $self->{Schema} ) {
3016 $schema->add_schema_object($schema_obj);
3017 }
3018- PTDEBUG && _d('Next schema object:', $schema_obj->{db}, $schema_obj->{tbl});
3019+ PTDEBUG && _d('Next schema object:',
3020+ $schema_obj->{db}, $schema_obj->{tbl});
3021 }
3022
3023 return $schema_obj;
3024 }
3025
3026 sub _iterate_files {
3027- my ( $self ) = @_;
3028+ my ( $self ) = @_;
3029
3030 if ( !$self->{fh} ) {
3031 my ($fh, $file) = $self->{file_itr}->();
3032@@ -4708,14 +4694,14 @@
3033 next CHUNK;
3034 }
3035 $ddl =~ s/ \*\/;\Z/;/; # remove end of version comment
3036-
3037- my ($engine) = $ddl =~ m/\).*?(?:ENGINE|TYPE)=(\w+)/;
3038-
3039- if ( !$engine || $self->engine_is_allowed($engine) ) {
3040+ my $tbl_struct = $self->{TableParser}->parse($ddl);
3041+ if ( $self->engine_is_allowed($tbl_struct->{engine}) ) {
3042 return {
3043- db => $self->{db},
3044- tbl => $tbl,
3045- ddl => $ddl,
3046+ db => $self->{db},
3047+ tbl => $tbl,
3048+ name => $self->{Quoter}->quote($self->{db}, $tbl),
3049+ ddl => $ddl,
3050+ tbl_struct => $tbl_struct,
3051 };
3052 }
3053 }
3054@@ -4732,6 +4718,7 @@
3055 sub _iterate_dbh {
3056 my ( $self ) = @_;
3057 my $q = $self->{Quoter};
3058+ my $tp = $self->{TableParser};
3059 my $dbh = $self->{dbh};
3060 PTDEBUG && _d('Getting next schema object from dbh', $dbh);
3061
3062@@ -4770,30 +4757,15 @@
3063 }
3064
3065 while ( my $tbl = shift @{$self->{tbls}} ) {
3066- my $tbl_status;
3067- if ( $self->{filters}->{'engines'}
3068- || $self->{filters}->{'ignore-engines'}
3069- || $self->{keep_tbl_status} )
3070- {
3071- my $sql = "SHOW TABLE STATUS FROM " . $q->quote($self->{db})
3072- . " LIKE \'$tbl\'";
3073- PTDEBUG && _d($sql);
3074- $tbl_status = $dbh->selectrow_hashref($sql);
3075- PTDEBUG && _d(Dumper($tbl_status));
3076- }
3077-
3078- if ( !$tbl_status
3079- || $self->engine_is_allowed($tbl_status->{engine}) ) {
3080- my $ddl;
3081- if ( my $tp = $self->{TableParser} ) {
3082- $ddl = $tp->get_create_table($dbh, $self->{db}, $tbl);
3083- }
3084-
3085+ my $ddl = $tp->get_create_table($dbh, $self->{db}, $tbl);
3086+ my $tbl_struct = $tp->parse($ddl);
3087+ if ( $self->engine_is_allowed($tbl_struct->{engine}) ) {
3088 return {
3089 db => $self->{db},
3090 tbl => $tbl,
3091+ name => $q->quote($self->{db}, $tbl),
3092 ddl => $ddl,
3093- tbl_status => $tbl_status,
3094+ tbl_struct => $tbl_struct,
3095 };
3096 }
3097 }
3098@@ -4897,7 +4869,11 @@
3099
3100 sub engine_is_allowed {
3101 my ( $self, $engine ) = @_;
3102- die "I need an engine argument" unless $engine;
3103+
3104+ if ( !$engine ) {
3105+ PTDEBUG && _d('No engine specified; allowing the table');
3106+ return 1;
3107+ }
3108
3109 $engine = lc $engine;
3110
3111@@ -6253,13 +6229,12 @@
3112 }
3113
3114 my $schema_iter = new SchemaIterator(
3115- dbh => $master_dbh,
3116- resume => $last_chunk ? $q->quote(@{$last_chunk}{qw(db tbl)})
3117- : "",
3118- keep_tbl_status => 1,
3119- OptionParser => $o,
3120- TableParser => $tp,
3121- Quoter => $q,
3122+ dbh => $master_dbh,
3123+ resume => $last_chunk ? $q->quote(@{$last_chunk}{qw(db tbl)})
3124+ : "",
3125+ OptionParser => $o,
3126+ TableParser => $tp,
3127+ Quoter => $q,
3128 );
3129
3130 if ( $last_chunk &&
3131@@ -6334,13 +6309,13 @@
3132 my $chunk_size_limit = $o->get('chunk-size-limit');
3133 my @too_large;
3134 foreach my $slave ( @$slaves ) {
3135+ # get_row_estimate() returns (row_est, index), but
3136+ # we only need the row_est. Maybe in the future we'll
3137+ # care what index MySQL will use on a slave.
3138 my ($n_rows) = NibbleIterator::get_row_estimate(
3139- Cxn => $slave,
3140- tbl => $tbl,
3141- where => $o->get('where') || "1=1",
3142- OptionParser => $o,
3143- TableParser => $tp,
3144- Quoter => $q,
3145+ Cxn => $slave,
3146+ tbl => $tbl,
3147+ where => $o->get('where'),
3148 );
3149 PTDEBUG && _d('Table on', $slave->name(),
3150 'has', $n_rows, 'rows');
3151
3152=== modified file 'bin/pt-table-sync'
3153--- bin/pt-table-sync 2012-03-30 16:53:51 +0000
3154+++ bin/pt-table-sync 2012-03-31 16:07:24 +0000
3155@@ -959,7 +959,7 @@
3156 $opt->{value} = ($pre || '') . $num;
3157 }
3158 else {
3159- $self->save_error("Invalid size for --$opt->{long}");
3160+ $self->save_error("Invalid size for --$opt->{long}: $val");
3161 }
3162 return;
3163 }
3164@@ -1285,12 +1285,14 @@
3165 sub as_string {
3166 my ( $self, $dsn, $props ) = @_;
3167 return $dsn unless ref $dsn;
3168- my %allowed = $props ? map { $_=>1 } @$props : ();
3169+ my @keys = $props ? @$props : sort keys %$dsn;
3170 return join(',',
3171- map { "$_=" . ($_ eq 'p' ? '...' : $dsn->{$_}) }
3172- grep { defined $dsn->{$_} && $self->{opts}->{$_} }
3173- grep { !$props || $allowed{$_} }
3174- sort keys %$dsn );
3175+ map { "$_=" . ($_ eq 'p' ? '...' : $dsn->{$_}) }
3176+ grep {
3177+ exists $self->{opts}->{$_}
3178+ && exists $dsn->{$_}
3179+ && defined $dsn->{$_}
3180+ } @keys);
3181 }
3182
3183 sub usage {
3184@@ -1741,19 +1743,58 @@
3185 return bless $self, $class;
3186 }
3187
3188+sub get_create_table {
3189+ my ( $self, $dbh, $db, $tbl ) = @_;
3190+ die "I need a dbh parameter" unless $dbh;
3191+ die "I need a db parameter" unless $db;
3192+ die "I need a tbl parameter" unless $tbl;
3193+ my $q = $self->{Quoter};
3194+
3195+ my $new_sql_mode
3196+ = '/*!40101 SET @OLD_SQL_MODE := @@SQL_MODE, '
3197+ . q{@@SQL_MODE := REPLACE(REPLACE(@@SQL_MODE, 'ANSI_QUOTES', ''), ',,', ','), }
3198+ . '@OLD_QUOTE := @@SQL_QUOTE_SHOW_CREATE, '
3199+ . '@@SQL_QUOTE_SHOW_CREATE := 1 */';
3200+
3201+ my $old_sql_mode = '/*!40101 SET @@SQL_MODE := @OLD_SQL_MODE, '
3202+ . '@@SQL_QUOTE_SHOW_CREATE := @OLD_QUOTE */';
3203+
3204+ PTDEBUG && _d($new_sql_mode);
3205+ eval { $dbh->do($new_sql_mode); };
3206+ PTDEBUG && $EVAL_ERROR && _d($EVAL_ERROR);
3207+
3208+ my $use_sql = 'USE ' . $q->quote($db);
3209+ PTDEBUG && _d($dbh, $use_sql);
3210+ $dbh->do($use_sql);
3211+
3212+ my $show_sql = "SHOW CREATE TABLE " . $q->quote($db, $tbl);
3213+ PTDEBUG && _d($show_sql);
3214+ my $href;
3215+ eval { $href = $dbh->selectrow_hashref($show_sql); };
3216+ if ( $EVAL_ERROR ) {
3217+ PTDEBUG && _d($EVAL_ERROR);
3218+
3219+ PTDEBUG && _d($old_sql_mode);
3220+ $dbh->do($old_sql_mode);
3221+
3222+ return;
3223+ }
3224+
3225+ PTDEBUG && _d($old_sql_mode);
3226+ $dbh->do($old_sql_mode);
3227+
3228+ my ($key) = grep { m/create (?:table|view)/i } keys %$href;
3229+ if ( !$key ) {
3230+ die "Error: no 'Create Table' or 'Create View' in result set from "
3231+ . "$show_sql: " . Dumper($href);
3232+ }
3233+
3234+ return $href->{$key};
3235+}
3236+
3237 sub parse {
3238 my ( $self, $ddl, $opts ) = @_;
3239 return unless $ddl;
3240- if ( ref $ddl eq 'ARRAY' ) {
3241- if ( lc $ddl->[0] eq 'table' ) {
3242- $ddl = $ddl->[1];
3243- }
3244- else {
3245- return {
3246- engine => 'VIEW',
3247- };
3248- }
3249- }
3250
3251 if ( $ddl !~ m/CREATE (?:TEMPORARY )?TABLE `/ ) {
3252 die "Cannot parse table definition; is ANSI quoting "
3253@@ -2060,41 +2101,31 @@
3254 return $ddl;
3255 }
3256
3257-sub remove_secondary_indexes {
3258- my ( $self, $ddl ) = @_;
3259- my $sec_indexes_ddl;
3260- my $tbl_struct = $self->parse($ddl);
3261-
3262- if ( ($tbl_struct->{engine} || '') =~ m/InnoDB/i ) {
3263- my $clustered_key = $tbl_struct->{clustered_key};
3264- $clustered_key ||= '';
3265-
3266- my @sec_indexes = map {
3267- my $key_def = $_->{ddl};
3268- $key_def =~ s/([\(\)])/\\$1/g;
3269- $ddl =~ s/\s+$key_def//i;
3270-
3271- my $key_ddl = "ADD $_->{ddl}";
3272- $key_ddl .= ',' unless $key_ddl =~ m/,$/;
3273- $key_ddl;
3274- }
3275- grep { $_->{name} ne $clustered_key }
3276- values %{$tbl_struct->{keys}};
3277- PTDEBUG && _d('Secondary indexes:', Dumper(\@sec_indexes));
3278-
3279- if ( @sec_indexes ) {
3280- $sec_indexes_ddl = join(' ', @sec_indexes);
3281- $sec_indexes_ddl =~ s/,$//;
3282- }
3283-
3284- $ddl =~ s/,(\n\) )/$1/s;
3285- }
3286- else {
3287- PTDEBUG && _d('Not removing secondary indexes from',
3288- $tbl_struct->{engine}, 'table');
3289- }
3290-
3291- return $ddl, $sec_indexes_ddl, $tbl_struct;
3292+sub get_table_status {
3293+ my ( $self, $dbh, $db, $like ) = @_;
3294+ my $q = $self->{Quoter};
3295+ my $sql = "SHOW TABLE STATUS FROM " . $q->quote($db);
3296+ my @params;
3297+ if ( $like ) {
3298+ $sql .= ' LIKE ?';
3299+ push @params, $like;
3300+ }
3301+ PTDEBUG && _d($sql, @params);
3302+ my $sth = $dbh->prepare($sql);
3303+ eval { $sth->execute(@params); };
3304+ if ($EVAL_ERROR) {
3305+ PTDEBUG && _d($EVAL_ERROR);
3306+ return;
3307+ }
3308+ my @tables = @{$sth->fetchall_arrayref({})};
3309+ @tables = map {
3310+ my %tbl; # Make a copy with lowercased keys
3311+ @tbl{ map { lc $_ } keys %$_ } = values %$_;
3312+ $tbl{engine} ||= $tbl{type} || $tbl{comment};
3313+ delete $tbl{type};
3314+ \%tbl;
3315+ } @tables;
3316+ return @tables;
3317 }
3318
3319 sub _d {
3320@@ -5445,11 +5476,12 @@
3321
3322 eval {
3323 if ( my $timeout = $args{wait} ) {
3324- my $wait = $args{wait_retry_args}->{wait} || 10;
3325+ my $ms = $self->{MasterSlave};
3326 my $tries = $args{wait_retry_args}->{tries} || 3;
3327+ my $wait;
3328 $self->{Retry}->retry(
3329- wait => sub { sleep $wait; },
3330 tries => $tries,
3331+ wait => sub { sleep $args{wait_retry_args}->{wait} || 10 },
3332 try => sub {
3333 my ( %args ) = @_;
3334
3335@@ -5457,12 +5489,18 @@
3336 warn "Retrying MASTER_POS_WAIT() for --wait $timeout...";
3337 }
3338
3339- my $ms = $self->{MasterSlave};
3340- my $wait = $ms->wait_for_master(
3341+ $wait = $ms->wait_for_master(
3342 master_status => $ms->get_master_status($src->{misc_dbh}),
3343 slave_dbh => $dst->{dbh},
3344 timeout => $timeout,
3345 );
3346+ if ( defined $wait->{result} && $wait->{result} != -1 ) {
3347+ return; # slave caught up
3348+ }
3349+ die; # call fail
3350+ },
3351+ fail => sub {
3352+ my (%args) = @_;
3353 if ( !defined $wait->{result} ) {
3354 my $msg;
3355 if ( $wait->{waited} ) {
3356@@ -5477,20 +5515,14 @@
3357 $msg .= " Sleeping $wait seconds then retrying "
3358 . ($tries - $args{tryno}) . " more times.";
3359 }
3360- warn $msg;
3361- return;
3362+ warn "$msg\n";
3363+ return 1; # call wait, call try
3364 }
3365 elsif ( $wait->{result} == -1 ) {
3366- die "Slave did not catch up to its master after waiting "
3367- . "$timeout seconds with MASTER_POS_WAIT. Try inceasing "
3368- . "the --wait time, or disable this feature by specifying "
3369- . "--wait 0.";
3370- }
3371- else {
3372- return $result; # slave caught up
3373+ return 0; # call final_fail
3374 }
3375 },
3376- on_failure => sub {
3377+ final_fail => sub {
3378 die "Slave did not catch up to its master after $tries attempts "
3379 . "of waiting $timeout seconds with MASTER_POS_WAIT. "
3380 . "Check that the slave is running, increase the --wait "
3381@@ -5603,23 +5635,21 @@
3382 die "I need a $arg argument" unless defined $args{$arg};
3383 }
3384 my ($tbl_struct, $index) = @args{@required_args};
3385- my @cols = $args{cols} ? @{$args{cols}} : @{$tbl_struct->{cols}};
3386+ my @cols = $args{cols} ? @{$args{cols}} : @{$tbl_struct->{cols}};
3387 my $q = $self->{Quoter};
3388
3389 die "Index '$index' does not exist in table"
3390 unless exists $tbl_struct->{keys}->{$index};
3391+ PTDEBUG && _d('Will ascend index', $index);
3392
3393 my @asc_cols = @{$tbl_struct->{keys}->{$index}->{cols}};
3394- my @asc_slice;
3395-
3396- @asc_cols = @{$tbl_struct->{keys}->{$index}->{cols}};
3397- PTDEBUG && _d('Will ascend index', $index);
3398- PTDEBUG && _d('Will ascend columns', join(', ', @asc_cols));
3399 if ( $args{asc_first} ) {
3400 @asc_cols = $asc_cols[0];
3401 PTDEBUG && _d('Ascending only first column');
3402 }
3403+ PTDEBUG && _d('Will ascend columns', join(', ', @asc_cols));
3404
3405+ my @asc_slice;
3406 my %col_posn = do { my $i = 0; map { $_ => $i++ } @cols };
3407 foreach my $col ( @asc_cols ) {
3408 if ( !exists $col_posn{$col} ) {
3409@@ -6720,292 +6750,427 @@
3410 # ###########################################################################
3411
3412 # ###########################################################################
3413-# SchemaIterator r7141
3414-# Don't update this package!
3415+# SchemaIterator package
3416+# This package is a copy without comments from the original. The original
3417+# with comments and its test file can be found in the Bazaar repository at,
3418+# lib/SchemaIterator.pm
3419+# t/lib/SchemaIterator.t
3420+# See https://launchpad.net/percona-toolkit for more information.
3421 # ###########################################################################
3422+{
3423 package SchemaIterator;
3424
3425 use strict;
3426 use warnings FATAL => 'all';
3427-
3428 use English qw(-no_match_vars);
3429+use constant PTDEBUG => $ENV{PTDEBUG} || 0;
3430+
3431 use Data::Dumper;
3432 $Data::Dumper::Indent = 1;
3433 $Data::Dumper::Sortkeys = 1;
3434 $Data::Dumper::Quotekeys = 0;
3435
3436-use constant PTDEBUG => $ENV{PTDEBUG} || 0;
3437+my $open_comment = qr{/\*!\d{5} };
3438+my $tbl_name = qr{
3439+ CREATE\s+
3440+ (?:TEMPORARY\s+)?
3441+ TABLE\s+
3442+ (?:IF NOT EXISTS\s+)?
3443+ ([^\(]+)
3444+}x;
3445+
3446
3447 sub new {
3448 my ( $class, %args ) = @_;
3449- foreach my $arg ( qw(Quoter) ) {
3450+ my @required_args = qw(OptionParser TableParser Quoter);
3451+ foreach my $arg ( @required_args ) {
3452 die "I need a $arg argument" unless $args{$arg};
3453 }
3454+
3455+ my ($file_itr, $dbh) = @args{qw(file_itr dbh)};
3456+ die "I need either a dbh or file_itr argument"
3457+ if (!$dbh && !$file_itr) || ($dbh && $file_itr);
3458+
3459+ my %resume;
3460+ if ( my $table = $args{resume} ) {
3461+ PTDEBUG && _d('Will resume from or after', $table);
3462+ my ($db, $tbl) = $args{Quoter}->split_unquote($table);
3463+ die "Resume table must be database-qualified: $table"
3464+ unless $db && $tbl;
3465+ $resume{db} = $db;
3466+ $resume{tbl} = $tbl;
3467+ }
3468+
3469 my $self = {
3470 %args,
3471- filter => undef,
3472- dbs => [],
3473+ resume => \%resume,
3474+ filters => _make_filters(%args),
3475 };
3476+
3477 return bless $self, $class;
3478 }
3479
3480-sub make_filter {
3481- my ( $self, $o ) = @_;
3482- my @lines = (
3483- 'sub {',
3484- ' my ( $dbh, $db, $tbl ) = @_;',
3485- ' my $engine = undef;',
3486- );
3487-
3488-
3489- my @permit_dbs = _make_filter('unless', '$db', $o->get('databases'))
3490- if $o->has('databases');
3491- my @reject_dbs = _make_filter('if', '$db', $o->get('ignore-databases'))
3492- if $o->has('ignore-databases');
3493- my @dbs_regex;
3494- if ( $o->has('databases-regex') && (my $p = $o->get('databases-regex')) ) {
3495- push @dbs_regex, " return 0 unless \$db && (\$db =~ m/$p/o);";
3496- }
3497- my @reject_dbs_regex;
3498- if ( $o->has('ignore-databases-regex')
3499- && (my $p = $o->get('ignore-databases-regex')) ) {
3500- push @reject_dbs_regex, " return 0 if \$db && (\$db =~ m/$p/o);";
3501- }
3502- if ( @permit_dbs || @reject_dbs || @dbs_regex || @reject_dbs_regex ) {
3503- push @lines,
3504- ' if ( $db ) {',
3505- (@permit_dbs ? @permit_dbs : ()),
3506- (@reject_dbs ? @reject_dbs : ()),
3507- (@dbs_regex ? @dbs_regex : ()),
3508- (@reject_dbs_regex ? @reject_dbs_regex : ()),
3509- ' }';
3510- }
3511-
3512- if ( $o->has('tables') || $o->has('ignore-tables')
3513- || $o->has('ignore-tables-regex') ) {
3514-
3515- my $have_qtbl = 0;
3516- my $have_only_qtbls = 0;
3517- my %qtbls;
3518-
3519- my @permit_tbls;
3520- my @permit_qtbls;
3521- my %permit_qtbls;
3522- if ( $o->get('tables') ) {
3523- my %tbls;
3524- map {
3525- if ( $_ =~ m/\./ ) {
3526- $permit_qtbls{$_} = 1;
3527- }
3528- else {
3529- $tbls{$_} = 1;
3530- }
3531- } keys %{ $o->get('tables') };
3532- @permit_tbls = _make_filter('unless', '$tbl', \%tbls);
3533- @permit_qtbls = _make_filter('unless', '$qtbl', \%permit_qtbls);
3534-
3535- if ( @permit_qtbls ) {
3536- push @lines,
3537- ' my $qtbl = ($db ? "$db." : "") . ($tbl ? $tbl : "");';
3538- $have_qtbl = 1;
3539- }
3540- }
3541-
3542- my @reject_tbls;
3543- my @reject_qtbls;
3544- my %reject_qtbls;
3545- if ( $o->get('ignore-tables') ) {
3546- my %tbls;
3547- map {
3548- if ( $_ =~ m/\./ ) {
3549- $reject_qtbls{$_} = 1;
3550- }
3551- else {
3552- $tbls{$_} = 1;
3553- }
3554- } keys %{ $o->get('ignore-tables') };
3555- @reject_tbls= _make_filter('if', '$tbl', \%tbls);
3556- @reject_qtbls = _make_filter('if', '$qtbl', \%reject_qtbls);
3557-
3558- if ( @reject_qtbls && !$have_qtbl ) {
3559- push @lines,
3560- ' my $qtbl = ($db ? "$db." : "") . ($tbl ? $tbl : "");';
3561- }
3562- }
3563-
3564- if ( keys %permit_qtbls && !@permit_dbs ) {
3565- my $dbs = {};
3566- map {
3567- my ($db, undef) = split(/\./, $_);
3568- $dbs->{$db} = 1;
3569- } keys %permit_qtbls;
3570- PTDEBUG && _d('Adding restriction "--databases',
3571- (join(',', keys %$dbs) . '"'));
3572- if ( keys %$dbs ) {
3573- $o->set('databases', $dbs);
3574- return $self->make_filter($o);
3575- }
3576- }
3577-
3578- my @tbls_regex;
3579- if ( $o->has('tables-regex') && (my $p = $o->get('tables-regex')) ) {
3580- push @tbls_regex, " return 0 unless \$tbl && (\$tbl =~ m/$p/o);";
3581- }
3582- my @reject_tbls_regex;
3583- if ( $o->has('ignore-tables-regex')
3584- && (my $p = $o->get('ignore-tables-regex')) ) {
3585- push @reject_tbls_regex,
3586- " return 0 if \$tbl && (\$tbl =~ m/$p/o);";
3587- }
3588-
3589- my @get_eng;
3590- my @permit_engs;
3591- my @reject_engs;
3592- if ( ($o->has('engines') && $o->get('engines'))
3593- || ($o->has('ignore-engines') && $o->get('ignore-engines')) ) {
3594- push @get_eng,
3595- ' my $sql = "SHOW TABLE STATUS "',
3596- ' . ($db ? "FROM `$db`" : "")',
3597- ' . " LIKE \'$tbl\'";',
3598- ' PTDEBUG && _d($sql);',
3599- ' eval {',
3600- ' $engine = $dbh->selectrow_hashref($sql)->{engine};',
3601- ' };',
3602- ' PTDEBUG && $EVAL_ERROR && _d($EVAL_ERROR);',
3603- ' PTDEBUG && _d($tbl, "uses engine", $engine);',
3604- ' $engine = lc $engine if $engine;',
3605- @permit_engs
3606- = _make_filter('unless', '$engine', $o->get('engines'), 1);
3607- @reject_engs
3608- = _make_filter('if', '$engine', $o->get('ignore-engines'), 1)
3609- }
3610-
3611- if ( @permit_tbls || @permit_qtbls || @reject_tbls || @tbls_regex
3612- || @reject_tbls_regex || @permit_engs || @reject_engs ) {
3613- push @lines,
3614- ' if ( $tbl ) {',
3615- (@permit_tbls ? @permit_tbls : ()),
3616- (@reject_tbls ? @reject_tbls : ()),
3617- (@tbls_regex ? @tbls_regex : ()),
3618- (@reject_tbls_regex ? @reject_tbls_regex : ()),
3619- (@permit_qtbls ? @permit_qtbls : ()),
3620- (@reject_qtbls ? @reject_qtbls : ()),
3621- (@get_eng ? @get_eng : ()),
3622- (@permit_engs ? @permit_engs : ()),
3623- (@reject_engs ? @reject_engs : ()),
3624- ' }';
3625- }
3626- }
3627-
3628- push @lines,
3629- ' PTDEBUG && _d(\'Passes filters:\', $db, $tbl, $engine, $dbh);',
3630- ' return 1;', '}';
3631-
3632- my $code = join("\n", @lines);
3633- PTDEBUG && _d('filter sub:', $code);
3634- my $filter_sub= eval $code
3635- or die "Error compiling subroutine code:\n$code\n$EVAL_ERROR";
3636-
3637- return $filter_sub;
3638-}
3639-
3640-sub set_filter {
3641- my ( $self, $filter_sub ) = @_;
3642- $self->{filter} = $filter_sub;
3643- PTDEBUG && _d('Set filter sub');
3644- return;
3645-}
3646-
3647-sub get_db_itr {
3648- my ( $self, %args ) = @_;
3649- my @required_args = qw(dbh);
3650+sub _make_filters {
3651+ my ( %args ) = @_;
3652+ my @required_args = qw(OptionParser Quoter);
3653 foreach my $arg ( @required_args ) {
3654 die "I need a $arg argument" unless $args{$arg};
3655 }
3656- my ($dbh) = @args{@required_args};
3657-
3658- my $filter = $self->{filter};
3659- my @dbs;
3660- eval {
3661+ my ($o, $q) = @args{@required_args};
3662+
3663+ my %filters;
3664+
3665+
3666+ my @simple_filters = qw(
3667+ databases tables engines
3668+ ignore-databases ignore-tables ignore-engines);
3669+ FILTER:
3670+ foreach my $filter ( @simple_filters ) {
3671+ if ( $o->has($filter) ) {
3672+ my $objs = $o->get($filter);
3673+ next FILTER unless $objs && scalar keys %$objs;
3674+ my $is_table = $filter =~ m/table/ ? 1 : 0;
3675+ foreach my $obj ( keys %$objs ) {
3676+ die "Undefined value for --$filter" unless $obj;
3677+ $obj = lc $obj;
3678+ if ( $is_table ) {
3679+ my ($db, $tbl) = $q->split_unquote($obj);
3680+ $db ||= '*';
3681+ PTDEBUG && _d('Filter', $filter, 'value:', $db, $tbl);
3682+ $filters{$filter}->{$tbl} = $db;
3683+ }
3684+ else { # database
3685+ PTDEBUG && _d('Filter', $filter, 'value:', $obj);
3686+ $filters{$filter}->{$obj} = 1;
3687+ }
3688+ }
3689+ }
3690+ }
3691+
3692+ my @regex_filters = qw(
3693+ databases-regex tables-regex
3694+ ignore-databases-regex ignore-tables-regex);
3695+ REGEX_FILTER:
3696+ foreach my $filter ( @regex_filters ) {
3697+ if ( $o->has($filter) ) {
3698+ my $pat = $o->get($filter);
3699+ next REGEX_FILTER unless $pat;
3700+ $filters{$filter} = qr/$pat/;
3701+ PTDEBUG && _d('Filter', $filter, 'value:', $filters{$filter});
3702+ }
3703+ }
3704+
3705+ PTDEBUG && _d('Schema object filters:', Dumper(\%filters));
3706+ return \%filters;
3707+}
3708+
3709+sub next {
3710+ my ( $self ) = @_;
3711+
3712+ if ( !$self->{initialized} ) {
3713+ $self->{initialized} = 1;
3714+ if ( $self->{resume}->{tbl}
3715+ && !$self->table_is_allowed(@{$self->{resume}}{qw(db tbl)}) ) {
3716+ PTDEBUG && _d('Will resume after',
3717+ join('.', @{$self->{resume}}{qw(db tbl)}));
3718+ $self->{resume}->{after} = 1;
3719+ }
3720+ }
3721+
3722+ my $schema_obj;
3723+ if ( $self->{file_itr} ) {
3724+ $schema_obj= $self->_iterate_files();
3725+ }
3726+ else { # dbh
3727+ $schema_obj= $self->_iterate_dbh();
3728+ }
3729+
3730+ if ( $schema_obj ) {
3731+ if ( my $schema = $self->{Schema} ) {
3732+ $schema->add_schema_object($schema_obj);
3733+ }
3734+ PTDEBUG && _d('Next schema object:',
3735+ $schema_obj->{db}, $schema_obj->{tbl});
3736+ }
3737+
3738+ return $schema_obj;
3739+}
3740+
3741+sub _iterate_files {
3742+ my ( $self ) = @_;
3743+
3744+ if ( !$self->{fh} ) {
3745+ my ($fh, $file) = $self->{file_itr}->();
3746+ if ( !$fh ) {
3747+ PTDEBUG && _d('No more files to iterate');
3748+ return;
3749+ }
3750+ $self->{fh} = $fh;
3751+ $self->{file} = $file;
3752+ }
3753+ my $fh = $self->{fh};
3754+ PTDEBUG && _d('Getting next schema object from', $self->{file});
3755+
3756+ local $INPUT_RECORD_SEPARATOR = '';
3757+ CHUNK:
3758+ while (defined(my $chunk = <$fh>)) {
3759+ if ($chunk =~ m/Database: (\S+)/) {
3760+ my $db = $1; # XXX
3761+ $db =~ s/^`//; # strip leading `
3762+ $db =~ s/`$//; # and trailing `
3763+ if ( $self->database_is_allowed($db)
3764+ && $self->_resume_from_database($db) ) {
3765+ $self->{db} = $db;
3766+ }
3767+ }
3768+ elsif ($self->{db} && $chunk =~ m/CREATE TABLE/) {
3769+ if ($chunk =~ m/DROP VIEW IF EXISTS/) {
3770+ PTDEBUG && _d('Table is a VIEW, skipping');
3771+ next CHUNK;
3772+ }
3773+
3774+ my ($tbl) = $chunk =~ m/$tbl_name/;
3775+ $tbl =~ s/^\s*`//;
3776+ $tbl =~ s/`\s*$//;
3777+ if ( $self->_resume_from_table($tbl)
3778+ && $self->table_is_allowed($self->{db}, $tbl) ) {
3779+ my ($ddl) = $chunk =~ m/^(?:$open_comment)?(CREATE TABLE.+?;)$/ms;
3780+ if ( !$ddl ) {
3781+ warn "Failed to parse CREATE TABLE from\n" . $chunk;
3782+ next CHUNK;
3783+ }
3784+ $ddl =~ s/ \*\/;\Z/;/; # remove end of version comment
3785+ my $tbl_struct = $self->{TableParser}->parse($ddl);
3786+ if ( $self->engine_is_allowed($tbl_struct->{engine}) ) {
3787+ return {
3788+ db => $self->{db},
3789+ tbl => $tbl,
3790+ name => $self->{Quoter}->quote($self->{db}, $tbl),
3791+ ddl => $ddl,
3792+ tbl_struct => $tbl_struct,
3793+ };
3794+ }
3795+ }
3796+ }
3797+ } # CHUNK
3798+
3799+ PTDEBUG && _d('No more schema objects in', $self->{file});
3800+ close $self->{fh};
3801+ $self->{fh} = undef;
3802+
3803+ return $self->_iterate_files();
3804+}
3805+
3806+sub _iterate_dbh {
3807+ my ( $self ) = @_;
3808+ my $q = $self->{Quoter};
3809+ my $tp = $self->{TableParser};
3810+ my $dbh = $self->{dbh};
3811+ PTDEBUG && _d('Getting next schema object from dbh', $dbh);
3812+
3813+ if ( !defined $self->{dbs} ) {
3814 my $sql = 'SHOW DATABASES';
3815 PTDEBUG && _d($sql);
3816- @dbs = grep {
3817- my $ok = $filter ? $filter->($dbh, $_, undef) : 1;
3818- $ok = 0 if $_ =~ m/information_schema|performance_schema|lost\+found/;
3819- $ok;
3820- } @{ $dbh->selectcol_arrayref($sql) };
3821+ my @dbs = grep { $self->database_is_allowed($_) }
3822+ @{$dbh->selectcol_arrayref($sql)};
3823 PTDEBUG && _d('Found', scalar @dbs, 'databases');
3824- };
3825-
3826- PTDEBUG && $EVAL_ERROR && _d($EVAL_ERROR);
3827- my $iterator = sub {
3828- return shift @dbs;
3829- };
3830-
3831- if (wantarray) {
3832- return ($iterator, scalar @dbs);
3833- }
3834- else {
3835- return $iterator;
3836- }
3837-}
3838-
3839-sub get_tbl_itr {
3840- my ( $self, %args ) = @_;
3841- my @required_args = qw(dbh db);
3842- foreach my $arg ( @required_args ) {
3843- die "I need a $arg argument" unless $args{$arg};
3844- }
3845- my ($dbh, $db, $views) = @args{@required_args, 'views'};
3846-
3847- my $filter = $self->{filter};
3848- my @tbls;
3849- if ( $db ) {
3850- eval {
3851- my $sql = 'SHOW /*!50002 FULL*/ TABLES FROM '
3852- . $self->{Quoter}->quote($db);
3853- PTDEBUG && _d($sql);
3854- @tbls = map {
3855- $_->[0]
3856- }
3857- grep {
3858- my ($tbl, $type) = @$_;
3859- my $ok = $filter ? $filter->($dbh, $db, $tbl) : 1;
3860- if ( !$views ) {
3861- $ok = 0 if ($type || '') eq 'VIEW';
3862- }
3863- $ok;
3864- }
3865- @{ $dbh->selectall_arrayref($sql) };
3866- PTDEBUG && _d('Found', scalar @tbls, 'tables in', $db);
3867- };
3868- PTDEBUG && $EVAL_ERROR && _d($EVAL_ERROR);
3869- }
3870- else {
3871- PTDEBUG && _d('No db given so no tables');
3872- }
3873-
3874- my $iterator = sub {
3875- return shift @tbls;
3876- };
3877-
3878- if ( wantarray ) {
3879- return ($iterator, scalar @tbls);
3880- }
3881- else {
3882- return $iterator;
3883- }
3884-}
3885-
3886-sub _make_filter {
3887- my ( $cond, $var_name, $objs, $lc ) = @_;
3888- my @lines;
3889- if ( scalar keys %$objs ) {
3890- my $test = join(' || ',
3891- map { "$var_name eq '" . ($lc ? lc $_ : $_) ."'" } keys %$objs);
3892- push @lines, " return 0 $cond $var_name && ($test);",
3893- }
3894- return @lines;
3895+ $self->{dbs} = \@dbs;
3896+ }
3897+
3898+ if ( !$self->{db} ) {
3899+ do {
3900+ $self->{db} = shift @{$self->{dbs}};
3901+ } until $self->_resume_from_database($self->{db});
3902+ PTDEBUG && _d('Next database:', $self->{db});
3903+ return unless $self->{db};
3904+ }
3905+
3906+ if ( !defined $self->{tbls} ) {
3907+ my $sql = 'SHOW /*!50002 FULL*/ TABLES FROM ' . $q->quote($self->{db});
3908+ PTDEBUG && _d($sql);
3909+ my @tbls = map {
3910+ $_->[0]; # (tbl, type)
3911+ }
3912+ grep {
3913+ my ($tbl, $type) = @$_;
3914+ (!$type || ($type ne 'VIEW'))
3915+ && $self->_resume_from_table($tbl)
3916+ && $self->table_is_allowed($self->{db}, $tbl);
3917+ }
3918+ @{$dbh->selectall_arrayref($sql)};
3919+ PTDEBUG && _d('Found', scalar @tbls, 'tables in database', $self->{db});
3920+ $self->{tbls} = \@tbls;
3921+ }
3922+
3923+ while ( my $tbl = shift @{$self->{tbls}} ) {
3924+ my $ddl = $tp->get_create_table($dbh, $self->{db}, $tbl);
3925+ my $tbl_struct = $tp->parse($ddl);
3926+ if ( $self->engine_is_allowed($tbl_struct->{engine}) ) {
3927+ return {
3928+ db => $self->{db},
3929+ tbl => $tbl,
3930+ name => $q->quote($self->{db}, $tbl),
3931+ ddl => $ddl,
3932+ tbl_struct => $tbl_struct,
3933+ };
3934+ }
3935+ }
3936+
3937+ PTDEBUG && _d('No more tables in database', $self->{db});
3938+ $self->{db} = undef;
3939+ $self->{tbls} = undef;
3940+
3941+ return $self->_iterate_dbh();
3942+}
3943+
3944+sub database_is_allowed {
3945+ my ( $self, $db ) = @_;
3946+ die "I need a db argument" unless $db;
3947+
3948+ $db = lc $db;
3949+
3950+ my $filter = $self->{filters};
3951+
3952+ if ( $db =~ m/information_schema|performance_schema|lost\+found/ ) {
3953+ PTDEBUG && _d('Database', $db, 'is a system database, ignoring');
3954+ return 0;
3955+ }
3956+
3957+ if ( $self->{filters}->{'ignore-databases'}->{$db} ) {
3958+ PTDEBUG && _d('Database', $db, 'is in --ignore-databases list');
3959+ return 0;
3960+ }
3961+
3962+ if ( $filter->{'ignore-databases-regex'}
3963+ && $db =~ $filter->{'ignore-databases-regex'} ) {
3964+ PTDEBUG && _d('Database', $db, 'matches --ignore-databases-regex');
3965+ return 0;
3966+ }
3967+
3968+ if ( $filter->{'databases'}
3969+ && !$filter->{'databases'}->{$db} ) {
3970+ PTDEBUG && _d('Database', $db, 'is not in --databases list, ignoring');
3971+ return 0;
3972+ }
3973+
3974+ if ( $filter->{'databases-regex'}
3975+ && $db !~ $filter->{'databases-regex'} ) {
3976+ PTDEBUG && _d('Database', $db, 'does not match --databases-regex, ignoring');
3977+ return 0;
3978+ }
3979+
3980+ return 1;
3981+}
3982+
3983+sub table_is_allowed {
3984+ my ( $self, $db, $tbl ) = @_;
3985+ die "I need a db argument" unless $db;
3986+ die "I need a tbl argument" unless $tbl;
3987+
3988+ $db = lc $db;
3989+ $tbl = lc $tbl;
3990+
3991+ my $filter = $self->{filters};
3992+
3993+ if ( $db eq 'mysql' && ($tbl eq 'general_log' || $tbl eq 'slow_log') ) {
3994+ return 0;
3995+ }
3996+
3997+ if ( $filter->{'ignore-tables'}->{$tbl}
3998+ && ($filter->{'ignore-tables'}->{$tbl} eq '*'
3999+ || $filter->{'ignore-tables'}->{$tbl} eq $db) ) {
4000+ PTDEBUG && _d('Table', $tbl, 'is in --ignore-tables list');
4001+ return 0;
4002+ }
4003+
4004+ if ( $filter->{'ignore-tables-regex'}
4005+ && $tbl =~ $filter->{'ignore-tables-regex'} ) {
4006+ PTDEBUG && _d('Table', $tbl, 'matches --ignore-tables-regex');
4007+ return 0;
4008+ }
4009+
4010+ if ( $filter->{'tables'}
4011+ && !$filter->{'tables'}->{$tbl} ) {
4012+ PTDEBUG && _d('Table', $tbl, 'is not in --tables list, ignoring');
4013+ return 0;
4014+ }
4015+
4016+ if ( $filter->{'tables-regex'}
4017+ && $tbl !~ $filter->{'tables-regex'} ) {
4018+ PTDEBUG && _d('Table', $tbl, 'does not match --tables-regex, ignoring');
4019+ return 0;
4020+ }
4021+
4022+ if ( $filter->{'tables'}
4023+ && $filter->{'tables'}->{$tbl}
4024+ && $filter->{'tables'}->{$tbl} ne '*'
4025+ && $filter->{'tables'}->{$tbl} ne $db ) {
4026+ PTDEBUG && _d('Table', $tbl, 'is only allowed in database',
4027+ $filter->{'tables'}->{$tbl});
4028+ return 0;
4029+ }
4030+
4031+ return 1;
4032+}
4033+
4034+sub engine_is_allowed {
4035+ my ( $self, $engine ) = @_;
4036+
4037+ if ( !$engine ) {
4038+ PTDEBUG && _d('No engine specified; allowing the table');
4039+ return 1;
4040+ }
4041+
4042+ $engine = lc $engine;
4043+
4044+ my $filter = $self->{filters};
4045+
4046+ if ( $filter->{'ignore-engines'}->{$engine} ) {
4047+ PTDEBUG && _d('Engine', $engine, 'is in --ignore-databases list');
4048+ return 0;
4049+ }
4050+
4051+ if ( $filter->{'engines'}
4052+ && !$filter->{'engines'}->{$engine} ) {
4053+ PTDEBUG && _d('Engine', $engine, 'is not in --engines list, ignoring');
4054+ return 0;
4055+ }
4056+
4057+ return 1;
4058+}
4059+
4060+sub _resume_from_database {
4061+ my ($self, $db) = @_;
4062+
4063+ return 1 unless $self->{resume}->{db};
4064+
4065+ if ( $db eq $self->{resume}->{db} ) {
4066+ PTDEBUG && _d('At resume db', $db);
4067+ delete $self->{resume}->{db};
4068+ return 1;
4069+ }
4070+
4071+ return 0;
4072+}
4073+
4074+sub _resume_from_table {
4075+ my ($self, $tbl) = @_;
4076+
4077+ return 1 unless $self->{resume}->{tbl};
4078+
4079+ if ( $tbl eq $self->{resume}->{tbl} ) {
4080+ if ( !$self->{resume}->{after} ) {
4081+ PTDEBUG && _d('Resuming from table', $tbl);
4082+ delete $self->{resume}->{tbl};
4083+ return 1;
4084+ }
4085+ else {
4086+ PTDEBUG && _d('Resuming after table', $tbl);
4087+ delete $self->{resume}->{tbl};
4088+ }
4089+ }
4090+
4091+ return 0;
4092 }
4093
4094 sub _d {
4095@@ -7017,7 +7182,7 @@
4096 }
4097
4098 1;
4099-
4100+}
4101 # ###########################################################################
4102 # End SchemaIterator package
4103 # ###########################################################################
4104@@ -7304,48 +7469,42 @@
4105
4106 sub retry {
4107 my ( $self, %args ) = @_;
4108- my @required_args = qw(try wait);
4109+ my @required_args = qw(try fail final_fail);
4110 foreach my $arg ( @required_args ) {
4111 die "I need a $arg argument" unless $args{$arg};
4112 };
4113- my ($try, $wait) = @args{@required_args};
4114+ my ($try, $fail, $final_fail) = @args{@required_args};
4115+ my $wait = $args{wait} || sub { sleep 1; };
4116 my $tries = $args{tries} || 3;
4117
4118+ my $last_error;
4119 my $tryno = 0;
4120+ TRY:
4121 while ( ++$tryno <= $tries ) {
4122- PTDEBUG && _d("Retry", $tryno, "of", $tries);
4123+ PTDEBUG && _d("Try", $tryno, "of", $tries);
4124 my $result;
4125 eval {
4126 $result = $try->(tryno=>$tryno);
4127 };
4128+ if ( $EVAL_ERROR ) {
4129+ PTDEBUG && _d("Try code failed:", $EVAL_ERROR);
4130+ $last_error = $EVAL_ERROR;
4131
4132- if ( defined $result ) {
4133+ if ( $tryno < $tries ) { # more retries
4134+ my $retry = $fail->(tryno=>$tryno, error=>$last_error);
4135+ last TRY unless $retry;
4136+ PTDEBUG && _d("Calling wait code");
4137+ $wait->(tryno=>$tryno);
4138+ }
4139+ }
4140+ else {
4141 PTDEBUG && _d("Try code succeeded");
4142- if ( my $on_success = $args{on_success} ) {
4143- PTDEBUG && _d("Calling on_success code");
4144- $on_success->(tryno=>$tryno, result=>$result);
4145- }
4146 return $result;
4147 }
4148-
4149- if ( $EVAL_ERROR ) {
4150- PTDEBUG && _d("Try code died:", $EVAL_ERROR);
4151- die $EVAL_ERROR unless $args{retry_on_die};
4152- }
4153-
4154- if ( $tryno < $tries ) {
4155- PTDEBUG && _d("Try code failed, calling wait code");
4156- $wait->(tryno=>$tryno);
4157- }
4158- }
4159-
4160- PTDEBUG && _d("Try code did not succeed");
4161- if ( my $on_failure = $args{on_failure} ) {
4162- PTDEBUG && _d("Calling on_failure code");
4163- $on_failure->();
4164- }
4165-
4166- return;
4167+ }
4168+
4169+ PTDEBUG && _d('Try code did not succeed');
4170+ return $final_fail->(error=>$last_error);
4171 }
4172
4173 sub _d {
4174@@ -8006,27 +8165,20 @@
4175 tbl => undef, # set later
4176 };
4177
4178- my $si = new SchemaIterator(
4179- Quoter => $args{Quoter},
4180+ my $schema_iter = new SchemaIterator(
4181+ dbh => $src->{dbh},
4182+ OptionParser => $o,
4183+ TableParser => $args{TableParser},
4184+ Quoter => $args{Quoter},
4185 );
4186- $si->set_filter($si->make_filter($o));
4187
4188 # Make a list of all dbs.tbls on the source. It's more efficient this
4189 # way because it avoids open/closing a dbh for each tbl and dsn, unless
4190 # we pre-opened the dsn. It would also cause confusing verbose output.
4191 my @dbs_tbls;
4192- my $next_db = $si->get_db_itr(dbh => $src->{dbh});
4193- while ( my $db = $next_db->() ) {
4194- PTDEBUG && _d('Getting tables from', $db);
4195- my $next_tbl = $si->get_tbl_itr(
4196- dbh => $src->{dbh},
4197- db => $db,
4198- views => 0,
4199- );
4200- while ( my $tbl = $next_tbl->() ) {
4201- PTDEBUG && _d('Got table', $tbl);
4202- push @dbs_tbls, { db => $db, tbl => $tbl };
4203- }
4204+ while ( my $tbl = $schema_iter->next() ) {
4205+ PTDEBUG && _d('Got table', $tbl->{db}, $tbl->{tbl});
4206+ push @dbs_tbls, $tbl;
4207 }
4208
4209 my $exit_status = 0;
4210@@ -8048,6 +8200,7 @@
4211 lock_server(src => $src, dst => $dst, %args);
4212
4213 foreach my $db_tbl ( @dbs_tbls ) {
4214+ $src->{tbl_struct} = $db_tbl->{tbl_struct};
4215 $src->{db} = $dst->{db} = $db_tbl->{db};
4216 $src->{tbl} = $dst->{tbl} = $db_tbl->{tbl};
4217
4218@@ -8194,8 +8347,9 @@
4219 $start_ts = get_server_time($src->{dbh}) if $o->get('verbose');
4220
4221 # This will either die if there's a problem or return the tbl struct.
4222- my $tbl_struct = ok_to_sync($src, $dst, %args);
4223-
4224+ ok_to_sync($src, $dst, %args);
4225+ my $tbl_struct = $src->{tbl_struct};
4226+
4227 if ( my $diff = $args{diff} ) {
4228 PTDEBUG && _d('Converting checksum diff to WHERE:', Dumper($diff));
4229 $args{where} = diff_where(
4230@@ -8568,35 +8722,30 @@
4231 }
4232 my ($src, $dst, $dp, $q, $vp, $tp, $du, $syncer, $o) = @args{@required_args};
4233
4234- # First things first: check that the src and dst dbs and tbls exist.
4235- # This can fail in cases like h=host,D=bad,t=also_bad (i.e. simple
4236- # user error). It can also fail when syncing all dbs/tbls with sync_all()
4237- # because the dst db/tbl is assumed to be the same as the src but
4238- # this isn't always the case.
4239- my $src_tbl_ddl;
4240- eval {
4241- # FYI: get_create_table() does USE db but doesn't eval it.
4242- $src->{dbh}->do("USE `$src->{db}`");
4243- $src_tbl_ddl = $du->get_create_table($src->{dbh}, $q,
4244- $src->{db}, $src->{tbl});
4245- };
4246- die $EVAL_ERROR if $EVAL_ERROR;
4247-
4248- my $dst_tbl_ddl;
4249- eval {
4250- # FYI: get_create_table() does USE db but doesn't eval it.
4251- $dst->{dbh}->do("USE `$dst->{db}`");
4252- $dst_tbl_ddl = $du->get_create_table($dst->{dbh}, $q,
4253- $dst->{db}, $dst->{tbl});
4254- };
4255- die $EVAL_ERROR if $EVAL_ERROR;
4256-
4257- # This doesn't work at the moment when syncing different table names.
4258- # Check that src.db.tbl has the exact same schema as dst.db.tbl.
4259- # if ( $o->get('check-schema') && ($src_tbl_ddl ne $dst_tbl_ddl) ) {
4260- # die "Source and destination tables have different schemas";
4261- # }
4262- my $tbl_struct = $tp->parse($src_tbl_ddl);
4263+ if ( !$src->{tbl_struct} ) {
4264+ eval {
4265+ $src->{ddl} = $tp->get_create_table(
4266+ $src->{dbh}, $src->{db}, $src->{tbl});
4267+ $src->{tbl_struct} = $tp->parse($src->{ddl});
4268+
4269+ };
4270+ if ( $EVAL_ERROR ) {
4271+ die "Error getting table structure for $src->{db}.$src->{tbl} on "
4272+ . $dp->as_string($src->{dsn}) . "$EVAL_ERROR\nEnsure that "
4273+ . "the table exists and is accessible.\n";
4274+ }
4275+ }
4276+
4277+ # Check that the dst has the table.
4278+ my $dst_has_table = $tp->check_table(
4279+ dbh => $dst->{dbh},
4280+ db => $dst->{db},
4281+ tbl => $dst->{tbl},
4282+ );
4283+ if ( !$dst_has_table ) {
4284+ die "Table $dst->{db}.$dst->{tbl} does not exist on "
4285+ . $dp->as_string($dst->{dsn}) . "\n";
4286+ }
4287
4288 # Check that the user has all the necessary privs on the tbls.
4289 if ( $o->get('check-privileges') ) {
4290@@ -8629,7 +8778,7 @@
4291 }
4292 }
4293
4294- return $tbl_struct;
4295+ return;
4296 }
4297
4298 # Sub: filter_diffs
4299
4300=== added file 'bin/pt-table-usage'
4301--- bin/pt-table-usage 1970-01-01 00:00:00 +0000
4302+++ bin/pt-table-usage 2012-03-31 16:07:24 +0000
4303@@ -0,0 +1,7320 @@
4304+#!/usr/bin/env perl
4305+
4306+# This program is part of Percona Toolkit: http://www.percona.com/software/
4307+# See "COPYRIGHT, LICENSE, AND WARRANTY" at the end of this file for legal
4308+# notices and disclaimers.
4309+
4310+use strict;
4311+use warnings FATAL => 'all';
4312+use constant PTDEBUG => $ENV{PTDEBUG} || 0;
4313+
4314+# ###########################################################################
4315+# DSNParser package
4316+# This package is a copy without comments from the original. The original
4317+# with comments and its test file can be found in the Bazaar repository at,
4318+# lib/DSNParser.pm
4319+# t/lib/DSNParser.t
4320+# See https://launchpad.net/percona-toolkit for more information.
4321+# ###########################################################################
4322+{
4323+package DSNParser;
4324+
4325+use strict;
4326+use warnings FATAL => 'all';
4327+use English qw(-no_match_vars);
4328+use constant PTDEBUG => $ENV{PTDEBUG} || 0;
4329+
4330+use Data::Dumper;
4331+$Data::Dumper::Indent = 0;
4332+$Data::Dumper::Quotekeys = 0;
4333+
4334+eval {
4335+ require DBI;
4336+};
4337+my $have_dbi = $EVAL_ERROR ? 0 : 1;
4338+
4339+sub new {
4340+ my ( $class, %args ) = @_;
4341+ foreach my $arg ( qw(opts) ) {
4342+ die "I need a $arg argument" unless $args{$arg};
4343+ }
4344+ my $self = {
4345+ opts => {} # h, P, u, etc. Should come from DSN OPTIONS section in POD.
4346+ };
4347+ foreach my $opt ( @{$args{opts}} ) {
4348+ if ( !$opt->{key} || !$opt->{desc} ) {
4349+ die "Invalid DSN option: ", Dumper($opt);
4350+ }
4351+ PTDEBUG && _d('DSN option:',
4352+ join(', ',
4353+ map { "$_=" . (defined $opt->{$_} ? ($opt->{$_} || '') : 'undef') }
4354+ keys %$opt
4355+ )
4356+ );
4357+ $self->{opts}->{$opt->{key}} = {
4358+ dsn => $opt->{dsn},
4359+ desc => $opt->{desc},
4360+ copy => $opt->{copy} || 0,
4361+ };
4362+ }
4363+ return bless $self, $class;
4364+}
4365+
4366+sub prop {
4367+ my ( $self, $prop, $value ) = @_;
4368+ if ( @_ > 2 ) {
4369+ PTDEBUG && _d('Setting', $prop, 'property');
4370+ $self->{$prop} = $value;
4371+ }
4372+ return $self->{$prop};
4373+}
4374+
4375+sub parse {
4376+ my ( $self, $dsn, $prev, $defaults ) = @_;
4377+ if ( !$dsn ) {
4378+ PTDEBUG && _d('No DSN to parse');
4379+ return;
4380+ }
4381+ PTDEBUG && _d('Parsing', $dsn);
4382+ $prev ||= {};
4383+ $defaults ||= {};
4384+ my %given_props;
4385+ my %final_props;
4386+ my $opts = $self->{opts};
4387+
4388+ foreach my $dsn_part ( split(/,/, $dsn) ) {
4389+ if ( my ($prop_key, $prop_val) = $dsn_part =~ m/^(.)=(.*)$/ ) {
4390+ $given_props{$prop_key} = $prop_val;
4391+ }
4392+ else {
4393+ PTDEBUG && _d('Interpreting', $dsn_part, 'as h=', $dsn_part);
4394+ $given_props{h} = $dsn_part;
4395+ }
4396+ }
4397+
4398+ foreach my $key ( keys %$opts ) {
4399+ PTDEBUG && _d('Finding value for', $key);
4400+ $final_props{$key} = $given_props{$key};
4401+ if ( !defined $final_props{$key}
4402+ && defined $prev->{$key} && $opts->{$key}->{copy} )
4403+ {
4404+ $final_props{$key} = $prev->{$key};
4405+ PTDEBUG && _d('Copying value for', $key, 'from previous DSN');
4406+ }
4407+ if ( !defined $final_props{$key} ) {
4408+ $final_props{$key} = $defaults->{$key};
4409+ PTDEBUG && _d('Copying value for', $key, 'from defaults');
4410+ }
4411+ }
4412+
4413+ foreach my $key ( keys %given_props ) {
4414+ die "Unknown DSN option '$key' in '$dsn'. For more details, "
4415+ . "please use the --help option, or try 'perldoc $PROGRAM_NAME' "
4416+ . "for complete documentation."
4417+ unless exists $opts->{$key};
4418+ }
4419+ if ( (my $required = $self->prop('required')) ) {
4420+ foreach my $key ( keys %$required ) {
4421+ die "Missing required DSN option '$key' in '$dsn'. For more details, "
4422+ . "please use the --help option, or try 'perldoc $PROGRAM_NAME' "
4423+ . "for complete documentation."
4424+ unless $final_props{$key};
4425+ }
4426+ }
4427+
4428+ return \%final_props;
4429+}
4430+
4431+sub parse_options {
4432+ my ( $self, $o ) = @_;
4433+ die 'I need an OptionParser object' unless ref $o eq 'OptionParser';
4434+ my $dsn_string
4435+ = join(',',
4436+ map { "$_=".$o->get($_); }
4437+ grep { $o->has($_) && $o->get($_) }
4438+ keys %{$self->{opts}}
4439+ );
4440+ PTDEBUG && _d('DSN string made from options:', $dsn_string);
4441+ return $self->parse($dsn_string);
4442+}
4443+
4444+sub as_string {
4445+ my ( $self, $dsn, $props ) = @_;
4446+ return $dsn unless ref $dsn;
4447+ my @keys = $props ? @$props : sort keys %$dsn;
4448+ return join(',',
4449+ map { "$_=" . ($_ eq 'p' ? '...' : $dsn->{$_}) }
4450+ grep {
4451+ exists $self->{opts}->{$_}
4452+ && exists $dsn->{$_}
4453+ && defined $dsn->{$_}
4454+ } @keys);
4455+}
4456+
4457+sub usage {
4458+ my ( $self ) = @_;
4459+ my $usage
4460+ = "DSN syntax is key=value[,key=value...] Allowable DSN keys:\n\n"
4461+ . " KEY COPY MEANING\n"
4462+ . " === ==== =============================================\n";
4463+ my %opts = %{$self->{opts}};
4464+ foreach my $key ( sort keys %opts ) {
4465+ $usage .= " $key "
4466+ . ($opts{$key}->{copy} ? 'yes ' : 'no ')
4467+ . ($opts{$key}->{desc} || '[No description]')
4468+ . "\n";
4469+ }
4470+ $usage .= "\n If the DSN is a bareword, the word is treated as the 'h' key.\n";
4471+ return $usage;
4472+}
4473+
4474+sub get_cxn_params {
4475+ my ( $self, $info ) = @_;
4476+ my $dsn;
4477+ my %opts = %{$self->{opts}};
4478+ my $driver = $self->prop('dbidriver') || '';
4479+ if ( $driver eq 'Pg' ) {
4480+ $dsn = 'DBI:Pg:dbname=' . ( $info->{D} || '' ) . ';'
4481+ . join(';', map { "$opts{$_}->{dsn}=$info->{$_}" }
4482+ grep { defined $info->{$_} }
4483+ qw(h P));
4484+ }
4485+ else {
4486+ $dsn = 'DBI:mysql:' . ( $info->{D} || '' ) . ';'
4487+ . join(';', map { "$opts{$_}->{dsn}=$info->{$_}" }
4488+ grep { defined $info->{$_} }
4489+ qw(F h P S A))
4490+ . ';mysql_read_default_group=client';
4491+ }
4492+ PTDEBUG && _d($dsn);
4493+ return ($dsn, $info->{u}, $info->{p});
4494+}
4495+
4496+sub fill_in_dsn {
4497+ my ( $self, $dbh, $dsn ) = @_;
4498+ my $vars = $dbh->selectall_hashref('SHOW VARIABLES', 'Variable_name');
4499+ my ($user, $db) = $dbh->selectrow_array('SELECT USER(), DATABASE()');
4500+ $user =~ s/@.*//;
4501+ $dsn->{h} ||= $vars->{hostname}->{Value};
4502+ $dsn->{S} ||= $vars->{'socket'}->{Value};
4503+ $dsn->{P} ||= $vars->{port}->{Value};
4504+ $dsn->{u} ||= $user;
4505+ $dsn->{D} ||= $db;
4506+}
4507+
4508+sub get_dbh {
4509+ my ( $self, $cxn_string, $user, $pass, $opts ) = @_;
4510+ $opts ||= {};
4511+ my $defaults = {
4512+ AutoCommit => 0,
4513+ RaiseError => 1,
4514+ PrintError => 0,
4515+ ShowErrorStatement => 1,
4516+ mysql_enable_utf8 => ($cxn_string =~ m/charset=utf8/i ? 1 : 0),
4517+ };
4518+ @{$defaults}{ keys %$opts } = values %$opts;
4519+
4520+ if ( $opts->{mysql_use_result} ) {
4521+ $defaults->{mysql_use_result} = 1;
4522+ }
4523+
4524+ if ( !$have_dbi ) {
4525+ die "Cannot connect to MySQL because the Perl DBI module is not "
4526+ . "installed or not found. Run 'perl -MDBI' to see the directories "
4527+ . "that Perl searches for DBI. If DBI is not installed, try:\n"
4528+ . " Debian/Ubuntu apt-get install libdbi-perl\n"
4529+ . " RHEL/CentOS yum install perl-DBI\n"
4530+ . " OpenSolaris pgk install pkg:/SUNWpmdbi\n";
4531+
4532+ }
4533+
4534+ my $dbh;
4535+ my $tries = 2;
4536+ while ( !$dbh && $tries-- ) {
4537+ PTDEBUG && _d($cxn_string, ' ', $user, ' ', $pass,
4538+ join(', ', map { "$_=>$defaults->{$_}" } keys %$defaults ));
4539+
4540+ eval {
4541+ $dbh = DBI->connect($cxn_string, $user, $pass, $defaults);
4542+
4543+ if ( $cxn_string =~ m/mysql/i ) {
4544+ my $sql;
4545+
4546+ $sql = 'SELECT @@SQL_MODE';
4547+ PTDEBUG && _d($dbh, $sql);
4548+ my ($sql_mode) = $dbh->selectrow_array($sql);
4549+
4550+ $sql = 'SET @@SQL_QUOTE_SHOW_CREATE = 1'
4551+ . '/*!40101, @@SQL_MODE=\'NO_AUTO_VALUE_ON_ZERO'
4552+ . ($sql_mode ? ",$sql_mode" : '')
4553+ . '\'*/';
4554+ PTDEBUG && _d($dbh, $sql);
4555+ $dbh->do($sql);
4556+
4557+ if ( my ($charset) = $cxn_string =~ m/charset=(\w+)/ ) {
4558+ $sql = "/*!40101 SET NAMES $charset*/";
4559+ PTDEBUG && _d($dbh, ':', $sql);
4560+ $dbh->do($sql);
4561+ PTDEBUG && _d('Enabling charset for STDOUT');
4562+ if ( $charset eq 'utf8' ) {
4563+ binmode(STDOUT, ':utf8')
4564+ or die "Can't binmode(STDOUT, ':utf8'): $OS_ERROR";
4565+ }
4566+ else {
4567+ binmode(STDOUT) or die "Can't binmode(STDOUT): $OS_ERROR";
4568+ }
4569+ }
4570+
4571+ if ( $self->prop('set-vars') ) {
4572+ $sql = "SET " . $self->prop('set-vars');
4573+ PTDEBUG && _d($dbh, ':', $sql);
4574+ $dbh->do($sql);
4575+ }
4576+ }
4577+ };
4578+ if ( !$dbh && $EVAL_ERROR ) {
4579+ PTDEBUG && _d($EVAL_ERROR);
4580+ if ( $EVAL_ERROR =~ m/not a compiled character set|character set utf8/ ) {
4581+ PTDEBUG && _d('Going to try again without utf8 support');
4582+ delete $defaults->{mysql_enable_utf8};
4583+ }
4584+ elsif ( $EVAL_ERROR =~ m/locate DBD\/mysql/i ) {
4585+ die "Cannot connect to MySQL because the Perl DBD::mysql module is "
4586+ . "not installed or not found. Run 'perl -MDBD::mysql' to see "
4587+ . "the directories that Perl searches for DBD::mysql. If "
4588+ . "DBD::mysql is not installed, try:\n"
4589+ . " Debian/Ubuntu apt-get install libdbd-mysql-perl\n"
4590+ . " RHEL/CentOS yum install perl-DBD-MySQL\n"
4591+ . " OpenSolaris pgk install pkg:/SUNWapu13dbd-mysql\n";
4592+ }
4593+ if ( !$tries ) {
4594+ die $EVAL_ERROR;
4595+ }
4596+ }
4597+ }
4598+
4599+ PTDEBUG && _d('DBH info: ',
4600+ $dbh,
4601+ Dumper($dbh->selectrow_hashref(
4602+ 'SELECT DATABASE(), CONNECTION_ID(), VERSION()/*!50038 , @@hostname*/')),
4603+ 'Connection info:', $dbh->{mysql_hostinfo},
4604+ 'Character set info:', Dumper($dbh->selectall_arrayref(
4605+ 'SHOW VARIABLES LIKE "character_set%"', { Slice => {}})),
4606+ '$DBD::mysql::VERSION:', $DBD::mysql::VERSION,
4607+ '$DBI::VERSION:', $DBI::VERSION,
4608+ );
4609+
4610+ return $dbh;
4611+}
4612+
4613+sub get_hostname {
4614+ my ( $self, $dbh ) = @_;
4615+ if ( my ($host) = ($dbh->{mysql_hostinfo} || '') =~ m/^(\w+) via/ ) {
4616+ return $host;
4617+ }
4618+ my ( $hostname, $one ) = $dbh->selectrow_array(
4619+ 'SELECT /*!50038 @@hostname, */ 1');
4620+ return $hostname;
4621+}
4622+
4623+sub disconnect {
4624+ my ( $self, $dbh ) = @_;
4625+ PTDEBUG && $self->print_active_handles($dbh);
4626+ $dbh->disconnect;
4627+}
4628+
4629+sub print_active_handles {
4630+ my ( $self, $thing, $level ) = @_;
4631+ $level ||= 0;
4632+ printf("# Active %sh: %s %s %s\n", ($thing->{Type} || 'undef'), "\t" x $level,
4633+ $thing, (($thing->{Type} || '') eq 'st' ? $thing->{Statement} || '' : ''))
4634+ or die "Cannot print: $OS_ERROR";
4635+ foreach my $handle ( grep {defined} @{ $thing->{ChildHandles} } ) {
4636+ $self->print_active_handles( $handle, $level + 1 );
4637+ }
4638+}
4639+
4640+sub copy {
4641+ my ( $self, $dsn_1, $dsn_2, %args ) = @_;
4642+ die 'I need a dsn_1 argument' unless $dsn_1;
4643+ die 'I need a dsn_2 argument' unless $dsn_2;
4644+ my %new_dsn = map {
4645+ my $key = $_;
4646+ my $val;
4647+ if ( $args{overwrite} ) {
4648+ $val = defined $dsn_1->{$key} ? $dsn_1->{$key} : $dsn_2->{$key};
4649+ }
4650+ else {
4651+ $val = defined $dsn_2->{$key} ? $dsn_2->{$key} : $dsn_1->{$key};
4652+ }
4653+ $key => $val;
4654+ } keys %{$self->{opts}};
4655+ return \%new_dsn;
4656+}
4657+
4658+sub _d {
4659+ my ($package, undef, $line) = caller 0;
4660+ @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; }
4661+ map { defined $_ ? $_ : 'undef' }
4662+ @_;
4663+ print STDERR "# $package:$line $PID ", join(' ', @_), "\n";
4664+}
4665+
4666+1;
4667+}
4668+# ###########################################################################
4669+# End DSNParser package
4670+# ###########################################################################
4671+
4672+# ###########################################################################
4673+# OptionParser package
4674+# This package is a copy without comments from the original. The original
4675+# with comments and its test file can be found in the Bazaar repository at,
4676+# lib/OptionParser.pm
4677+# t/lib/OptionParser.t
4678+# See https://launchpad.net/percona-toolkit for more information.
4679+# ###########################################################################
4680+{
4681+package OptionParser;
4682+
4683+use strict;
4684+use warnings FATAL => 'all';
4685+use English qw(-no_match_vars);
4686+use constant PTDEBUG => $ENV{PTDEBUG} || 0;
4687+
4688+use List::Util qw(max);
4689+use Getopt::Long;
4690+
4691+my $POD_link_re = '[LC]<"?([^">]+)"?>';
4692+
4693+sub new {
4694+ my ( $class, %args ) = @_;
4695+ my @required_args = qw();
4696+ foreach my $arg ( @required_args ) {
4697+ die "I need a $arg argument" unless $args{$arg};
4698+ }
4699+
4700+ my ($program_name) = $PROGRAM_NAME =~ m/([.A-Za-z-]+)$/;
4701+ $program_name ||= $PROGRAM_NAME;
4702+ my $home = $ENV{HOME} || $ENV{HOMEPATH} || $ENV{USERPROFILE} || '.';
4703+
4704+ my %attributes = (
4705+ 'type' => 1,
4706+ 'short form' => 1,
4707+ 'group' => 1,
4708+ 'default' => 1,
4709+ 'cumulative' => 1,
4710+ 'negatable' => 1,
4711+ );
4712+
4713+ my $self = {
4714+ head1 => 'OPTIONS', # These args are used internally
4715+ skip_rules => 0, # to instantiate another Option-
4716+ item => '--(.*)', # Parser obj that parses the
4717+ attributes => \%attributes, # DSN OPTIONS section. Tools
4718+ parse_attributes => \&_parse_attribs, # don't tinker with these args.
4719+
4720+ %args,
4721+
4722+ strict => 1, # disabled by a special rule
4723+ program_name => $program_name,
4724+ opts => {},
4725+ got_opts => 0,
4726+ short_opts => {},
4727+ defaults => {},
4728+ groups => {},
4729+ allowed_groups => {},
4730+ errors => [],
4731+ rules => [], # desc of rules for --help
4732+ mutex => [], # rule: opts are mutually exclusive
4733+ atleast1 => [], # rule: at least one opt is required
4734+ disables => {}, # rule: opt disables other opts
4735+ defaults_to => {}, # rule: opt defaults to value of other opt
4736+ DSNParser => undef,
4737+ default_files => [
4738+ "/etc/percona-toolkit/percona-toolkit.conf",
4739+ "/etc/percona-toolkit/$program_name.conf",
4740+ "$home/.percona-toolkit.conf",
4741+ "$home/.$program_name.conf",
4742+ ],
4743+ types => {
4744+ string => 's', # standard Getopt type
4745+ int => 'i', # standard Getopt type
4746+ float => 'f', # standard Getopt type
4747+ Hash => 'H', # hash, formed from a comma-separated list
4748+ hash => 'h', # hash as above, but only if a value is given
4749+ Array => 'A', # array, similar to Hash
4750+ array => 'a', # array, similar to hash
4751+ DSN => 'd', # DSN
4752+ size => 'z', # size with kMG suffix (powers of 2^10)
4753+ time => 'm', # time, with an optional suffix of s/h/m/d
4754+ },
4755+ };
4756+
4757+ return bless $self, $class;
4758+}
4759+
4760+sub get_specs {
4761+ my ( $self, $file ) = @_;
4762+ $file ||= $self->{file} || __FILE__;
4763+ my @specs = $self->_pod_to_specs($file);
4764+ $self->_parse_specs(@specs);
4765+
4766+ open my $fh, "<", $file or die "Cannot open $file: $OS_ERROR";
4767+ my $contents = do { local $/ = undef; <$fh> };
4768+ close $fh;
4769+ if ( $contents =~ m/^=head1 DSN OPTIONS/m ) {
4770+ PTDEBUG && _d('Parsing DSN OPTIONS');
4771+ my $dsn_attribs = {
4772+ dsn => 1,
4773+ copy => 1,
4774+ };
4775+ my $parse_dsn_attribs = sub {
4776+ my ( $self, $option, $attribs ) = @_;
4777+ map {
4778+ my $val = $attribs->{$_};
4779+ if ( $val ) {
4780+ $val = $val eq 'yes' ? 1
4781+ : $val eq 'no' ? 0
4782+ : $val;
4783+ $attribs->{$_} = $val;
4784+ }
4785+ } keys %$attribs;
4786+ return {
4787+ key => $option,
4788+ %$attribs,
4789+ };
4790+ };
4791+ my $dsn_o = new OptionParser(
4792+ description => 'DSN OPTIONS',
4793+ head1 => 'DSN OPTIONS',
4794+ dsn => 0, # XXX don't infinitely recurse!
4795+ item => '\* (.)', # key opts are a single character
4796+ skip_rules => 1, # no rules before opts
4797+ attributes => $dsn_attribs,
4798+ parse_attributes => $parse_dsn_attribs,
4799+ );
4800+ my @dsn_opts = map {
4801+ my $opts = {
4802+ key => $_->{spec}->{key},
4803+ dsn => $_->{spec}->{dsn},
4804+ copy => $_->{spec}->{copy},
4805+ desc => $_->{desc},
4806+ };
4807+ $opts;
4808+ } $dsn_o->_pod_to_specs($file);
4809+ $self->{DSNParser} = DSNParser->new(opts => \@dsn_opts);
4810+ }
4811+
4812+ if ( $contents =~ m/^=head1 VERSION\n\n^(.+)$/m ) {
4813+ $self->{version} = $1;
4814+ PTDEBUG && _d($self->{version});
4815+ }
4816+
4817+ return;
4818+}
4819+
4820+sub DSNParser {
4821+ my ( $self ) = @_;
4822+ return $self->{DSNParser};
4823+};
4824+
4825+sub get_defaults_files {
4826+ my ( $self ) = @_;
4827+ return @{$self->{default_files}};
4828+}
4829+
4830+sub _pod_to_specs {
4831+ my ( $self, $file ) = @_;
4832+ $file ||= $self->{file} || __FILE__;
4833+ open my $fh, '<', $file or die "Cannot open $file: $OS_ERROR";
4834+
4835+ my @specs = ();
4836+ my @rules = ();
4837+ my $para;
4838+
4839+ local $INPUT_RECORD_SEPARATOR = '';
4840+ while ( $para = <$fh> ) {
4841+ next unless $para =~ m/^=head1 $self->{head1}/;
4842+ last;
4843+ }
4844+
4845+ while ( $para = <$fh> ) {
4846+ last if $para =~ m/^=over/;
4847+ next if $self->{skip_rules};
4848+ chomp $para;
4849+ $para =~ s/\s+/ /g;
4850+ $para =~ s/$POD_link_re/$1/go;
4851+ PTDEBUG && _d('Option rule:', $para);
4852+ push @rules, $para;
4853+ }
4854+
4855+ die "POD has no $self->{head1} section" unless $para;
4856+
4857+ do {
4858+ if ( my ($option) = $para =~ m/^=item $self->{item}/ ) {
4859+ chomp $para;
4860+ PTDEBUG && _d($para);
4861+ my %attribs;
4862+
4863+ $para = <$fh>; # read next paragraph, possibly attributes
4864+
4865+ if ( $para =~ m/: / ) { # attributes
4866+ $para =~ s/\s+\Z//g;
4867+ %attribs = map {
4868+ my ( $attrib, $val) = split(/: /, $_);
4869+ die "Unrecognized attribute for --$option: $attrib"
4870+ unless $self->{attributes}->{$attrib};
4871+ ($attrib, $val);
4872+ } split(/; /, $para);
4873+ if ( $attribs{'short form'} ) {
4874+ $attribs{'short form'} =~ s/-//;
4875+ }
4876+ $para = <$fh>; # read next paragraph, probably short help desc
4877+ }
4878+ else {
4879+ PTDEBUG && _d('Option has no attributes');
4880+ }
4881+
4882+ $para =~ s/\s+\Z//g;
4883+ $para =~ s/\s+/ /g;
4884+ $para =~ s/$POD_link_re/$1/go;
4885+
4886+ $para =~ s/\.(?:\n.*| [A-Z].*|\Z)//s;
4887+ PTDEBUG && _d('Short help:', $para);
4888+
4889+ die "No description after option spec $option" if $para =~ m/^=item/;
4890+
4891+ if ( my ($base_option) = $option =~ m/^\[no\](.*)/ ) {
4892+ $option = $base_option;
4893+ $attribs{'negatable'} = 1;
4894+ }
4895+
4896+ push @specs, {
4897+ spec => $self->{parse_attributes}->($self, $option, \%attribs),
4898+ desc => $para
4899+ . (defined $attribs{default} ? " (default $attribs{default})" : ''),
4900+ group => ($attribs{'group'} ? $attribs{'group'} : 'default'),
4901+ };
4902+ }
4903+ while ( $para = <$fh> ) {
4904+ last unless $para;
4905+ if ( $para =~ m/^=head1/ ) {
4906+ $para = undef; # Can't 'last' out of a do {} block.
4907+ last;
4908+ }
4909+ last if $para =~ m/^=item /;
4910+ }
4911+ } while ( $para );
4912+
4913+ die "No valid specs in $self->{head1}" unless @specs;
4914+
4915+ close $fh;
4916+ return @specs, @rules;
4917+}
4918+
4919+sub _parse_specs {
4920+ my ( $self, @specs ) = @_;
4921+ my %disables; # special rule that requires deferred checking
4922+
4923+ foreach my $opt ( @specs ) {
4924+ if ( ref $opt ) { # It's an option spec, not a rule.
4925+ PTDEBUG && _d('Parsing opt spec:',
4926+ map { ($_, '=>', $opt->{$_}) } keys %$opt);
4927+
4928+ my ( $long, $short ) = $opt->{spec} =~ m/^([\w-]+)(?:\|([^!+=]*))?/;
4929+ if ( !$long ) {
4930+ die "Cannot parse long option from spec $opt->{spec}";
4931+ }
4932+ $opt->{long} = $long;
4933+
4934+ die "Duplicate long option --$long" if exists $self->{opts}->{$long};
4935+ $self->{opts}->{$long} = $opt;
4936+
4937+ if ( length $long == 1 ) {
4938+ PTDEBUG && _d('Long opt', $long, 'looks like short opt');
4939+ $self->{short_opts}->{$long} = $long;
4940+ }
4941+
4942+ if ( $short ) {
4943+ die "Duplicate short option -$short"
4944+ if exists $self->{short_opts}->{$short};
4945+ $self->{short_opts}->{$short} = $long;
4946+ $opt->{short} = $short;
4947+ }
4948+ else {
4949+ $opt->{short} = undef;
4950+ }
4951+
4952+ $opt->{is_negatable} = $opt->{spec} =~ m/!/ ? 1 : 0;
4953+ $opt->{is_cumulative} = $opt->{spec} =~ m/\+/ ? 1 : 0;
4954+ $opt->{is_required} = $opt->{desc} =~ m/required/ ? 1 : 0;
4955+
4956+ $opt->{group} ||= 'default';
4957+ $self->{groups}->{ $opt->{group} }->{$long} = 1;
4958+
4959+ $opt->{value} = undef;
4960+ $opt->{got} = 0;
4961+
4962+ my ( $type ) = $opt->{spec} =~ m/=(.)/;
4963+ $opt->{type} = $type;
4964+ PTDEBUG && _d($long, 'type:', $type);
4965+
4966+
4967+ $opt->{spec} =~ s/=./=s/ if ( $type && $type =~ m/[HhAadzm]/ );
4968+
4969+ if ( (my ($def) = $opt->{desc} =~ m/default\b(?: ([^)]+))?/) ) {
4970+ $self->{defaults}->{$long} = defined $def ? $def : 1;
4971+ PTDEBUG && _d($long, 'default:', $def);
4972+ }
4973+
4974+ if ( $long eq 'config' ) {
4975+ $self->{defaults}->{$long} = join(',', $self->get_defaults_files());
4976+ }
4977+
4978+ if ( (my ($dis) = $opt->{desc} =~ m/(disables .*)/) ) {
4979+ $disables{$long} = $dis;
4980+ PTDEBUG && _d('Deferring check of disables rule for', $opt, $dis);
4981+ }
4982+
4983+ $self->{opts}->{$long} = $opt;
4984+ }
4985+ else { # It's an option rule, not a spec.
4986+ PTDEBUG && _d('Parsing rule:', $opt);
4987+ push @{$self->{rules}}, $opt;
4988+ my @participants = $self->_get_participants($opt);
4989+ my $rule_ok = 0;
4990+
4991+ if ( $opt =~ m/mutually exclusive|one and only one/ ) {
4992+ $rule_ok = 1;
4993+ push @{$self->{mutex}}, \@participants;
4994+ PTDEBUG && _d(@participants, 'are mutually exclusive');
4995+ }
4996+ if ( $opt =~ m/at least one|one and only one/ ) {
4997+ $rule_ok = 1;
4998+ push @{$self->{atleast1}}, \@participants;
4999+ PTDEBUG && _d(@participants, 'require at least one');
5000+ }
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches