Merge lp:~percona-toolkit-dev/percona-toolkit/pt-table-usage into lp:percona-toolkit/2.1

Proposed by Daniel Nichter
Status: Merged
Merged at revision: 225
Proposed branch: lp:~percona-toolkit-dev/percona-toolkit/pt-table-usage
Merge into: lp:percona-toolkit/2.1
Diff against target: 9952 lines (+9746/-8)
22 files modified
bin/pt-table-usage (+7320/-0)
lib/SQLParser.pm (+98/-8)
lib/TableUsage.pm (+1060/-0)
t/lib/TableUsage.t (+817/-0)
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/pt-table-usage
Reviewer Review Type Date Requested Status
Daniel Nichter Approve
Review via email: mp+100256@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Daniel Nichter (daniel-nichter) :
review: Approve

Preview Diff

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

Subscribers

People subscribed via source and target branches