Merge lp:~percona-toolkit-dev/percona-toolkit/pt-table-usage into lp:percona-toolkit/2.1
- pt-table-usage
- Merge into 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 |
Related bugs: | |
Related blueprints: |
Add pt-table-usage
(Medium)
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Daniel Nichter | Approve | ||
Review via email:
|
Commit message
Description of the change
To post a comment you must log in.
Revision history for this message
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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.