File Coverage

blib/lib/DBIx/QueryLog.pm
Criterion Covered Total %
statement 278 300 92.6
branch 93 130 71.5
condition 104 156 66.6
subroutine 44 44 100.0
pod 3 3 100.0
total 522 633 82.4


line stmt bran cond sub pod time code
1             package DBIx::QueryLog;
2              
3 23     23   1746029 use strict;
  23         52  
  23         918  
4 23     23   99 use warnings;
  23         36  
  23         609  
5 23     23   565 use 5.008_001;
  23         80  
  23         828  
6              
7 23     23   4644 use DBI;
  23         45569  
  23         1103  
8 23     23   2329 use Time::HiRes qw(gettimeofday tv_interval);
  23         5181  
  23         170  
9 23     23   20558 use Term::ANSIColor qw(colored);
  23         145772  
  23         10771  
10 23     23   17826 use Text::ASCIITable;
  23         339938  
  23         1335  
11 23     23   2252 use Data::Dumper ();
  23         14564  
  23         1215  
12              
13             $ENV{ANSI_COLORS_DISABLED} = 1 if $^O eq 'MSWin32';
14              
15             our $VERSION = '0.40';
16              
17 23     23   125 use constant _ORG_EXECUTE => \&DBI::st::execute;
  23         42  
  23         1820  
18 23     23   308 use constant _ORG_BIND_PARAM => \&DBI::st::bind_param;
  23         39  
  23         1167  
19 23     23   104 use constant _ORG_DB_DO => \&DBI::db::do;
  23         29  
  23         1135  
20 23     23   111 use constant _ORG_DB_SELECTALL_ARRAYREF => \&DBI::db::selectall_arrayref;
  23         33  
  23         1326  
21 23     23   108 use constant _ORG_DB_SELECTROW_ARRAYREF => \&DBI::db::selectrow_arrayref;
  23         31  
  23         1129  
22 23     23   108 use constant _ORG_DB_SELECTROW_ARRAY => \&DBI::db::selectrow_array;
  23         30  
  23         1332  
23              
24 23 50   23   105 use constant _HAS_MYSQL => eval { require DBD::mysql; 1 } ? 1 : 0;
  23         28  
  23         36  
  23         6732  
  0         0  
25 23 50   23   104 use constant _HAS_PG => eval { require DBD::Pg; 1 } ? 1 : 0;
  23         31  
  23         30  
  23         5971  
  0         0  
26 23 50   23   117 use constant _HAS_SQLITE => eval { require DBD::SQLite; DBD::SQLite->VERSION(1.48); 1 } ? 1 : 0;
  23         33  
  23         33  
  23         2497  
  23         22604  
  23         1785  
27 23 50   23   145 use constant _PP_MODE => $INC{'DBI/PurePerl.pm'} ? 1 : 0;
  23         47  
  23         5804  
28              
29             our %SKIP_PKG_MAP = (
30             'DBIx::QueryLog' => 1,
31             );
32             our $LOG_LEVEL = 'debug';
33             our $OUTPUT = *STDERR;
34              
35             my $st_execute;
36             my $st_bind_param;
37             my $db_do;
38             my $selectall_arrayref;
39             my $selectrow_arrayref;
40             my $selectrow_array;
41              
42             my $is_enabled = 0;
43              
44             sub import {
45 24     24   8288 my ($class) = @_;
46              
47 24   66     177 $st_execute ||= $class->_st_execute(_ORG_EXECUTE);
48 24   66     139 $st_bind_param ||= $class->_st_bind_param(_ORG_BIND_PARAM);
49 24   66     136 $db_do ||= $class->_db_do(_ORG_DB_DO) if _HAS_MYSQL or _HAS_PG or _HAS_SQLITE;
50 24         23 unless (_PP_MODE) {
51 24   66     123 $selectall_arrayref ||= $class->_select_array(_ORG_DB_SELECTALL_ARRAYREF);
52 24   66     109 $selectrow_arrayref ||= $class->_select_array(_ORG_DB_SELECTROW_ARRAYREF);
53 24   66     136 $selectrow_array ||= $class->_select_array(_ORG_DB_SELECTROW_ARRAY, 1);
54             }
55              
56 23     23   124 no warnings qw(redefine prototype);
  23         32  
  23         3358  
57 24         79 *DBI::st::execute = $st_execute;
58 24         49 *DBI::st::bind_param = $st_bind_param;
59 24         67 *DBI::db::do = $db_do if _HAS_MYSQL or _HAS_PG or _HAS_SQLITE;
60 24         34 unless (_PP_MODE) {
61 24         42 *DBI::db::selectall_arrayref = $selectall_arrayref;
62 24         54 *DBI::db::selectrow_arrayref = $selectrow_arrayref;
63 24         57 *DBI::db::selectrow_array = $selectrow_array;
64             }
65              
66 24         26379 $is_enabled = 1;
67             }
68              
69             sub unimport {
70 23     23   104 no warnings qw(redefine prototype);
  23         33  
  23         6370  
71 5     5   32736 *DBI::st::execute = _ORG_EXECUTE;
72 5         10 *DBI::st::bind_param = _ORG_BIND_PARAM;
73 5         16 *DBI::db::do = _ORG_DB_DO if _HAS_MYSQL or _HAS_PG or _HAS_SQLITE;
74 5         5 unless (_PP_MODE) {
75 5         10 *DBI::db::selectall_arrayref = _ORG_DB_SELECTALL_ARRAYREF;
76 5         8 *DBI::db::selectrow_arrayref = _ORG_DB_SELECTROW_ARRAYREF;
77 5         14 *DBI::db::selectrow_array = _ORG_DB_SELECTROW_ARRAY;
78             }
79              
80 5         14 $is_enabled = 0;
81             }
82              
83             *enable = *begin = \&import;
84             *disable = *end = \&unimport;
85              
86             sub guard {
87 1     1 1 4050 my $org_is_enabled = DBIx::QueryLog->is_enabled;
88 1         4 DBIx::QueryLog->enable();
89 1         5 return DBIx::QueryLog::Guard->new($org_is_enabled);
90             }
91              
92             sub ignore_trace {
93 1     1 1 8 my $org_is_enabled = DBIx::QueryLog->is_enabled;
94 1         4 DBIx::QueryLog->disable();
95 1         6 return DBIx::QueryLog::Guard->new($org_is_enabled);
96             }
97              
98 2     2 1 5 sub is_enabled { $is_enabled }
99              
100             my $container = {};
101             for my $accessor (qw{
102             logger threshold probability skip_bind
103             color useqq compact explain show_data_source
104             }) {
105 23     23   123 no strict 'refs';
  23         34  
  23         1288  
106             *{__PACKAGE__."::$accessor"} = sub {
107 23     23   98 use strict 'refs';
  23         28  
  23         12661  
108 53     53   38925 my ($class, $args) = @_;
109 53 100       277 return $container->{$accessor} unless @_ > 1;
110 36         159 $container->{$accessor} = $args;
111             };
112             }
113              
114             sub _st_execute {
115 22     22   43 my ($class, $org) = @_;
116              
117             return sub {
118 15 50   15   5289 my $wantarray = wantarray ? 1 : 0;
119 15         27 my $sth = shift;
120 15         61 my @params = @_;
121 15         18 my @types;
122              
123 15   33     81 my $probability = $container->{probability} || $ENV{DBIX_QUERYLOG_PROBABILITY};
124 15 50 33     44 if ($probability && int(rand() * $probability) % $probability != 0) {
125 0         0 return $org->($sth, @params);
126             }
127              
128 15         72 my $dbh = $sth->{Database};
129 15         68 my $ret = $sth->{Statement};
130 15 100       302 if (my $attrs = $sth->{private_DBIx_QueryLog_attrs}) {
131 5         13 my $bind_params = $sth->{private_DBIx_QueryLog_params};
132 5         14 for my $i (1..@$attrs) {
133 6         13 push @types, $attrs->[$i - 1]{TYPE};
134 6 50       21 push @params, $bind_params->[$i - 1] if $bind_params;
135             }
136             }
137             # DBD::Pg::st warns "undef in subroutine"
138 15 50       247 $sth->{private_DBIx_QueryLog_params} = $dbh->{Driver}{Name} eq 'Pg' ? '' : undef;
139              
140 15         33 my $explain;
141 15 100 100     87 if ($container->{explain} || $ENV{DBIX_QUERYLOG_EXPLAIN}) {
142 2         7 $explain = _explain($dbh, $ret, \@params, \@types);
143             }
144              
145 15 100 100     113 unless (($container->{skip_bind} || $ENV{DBIX_QUERYLOG_SKIP_BIND}) && @params) {
      66        
146 9         30 $ret = _bind($dbh, $ret, \@params, \@types);
147             }
148              
149 15         81 my $begin = [gettimeofday];
150 15 50       717 my $res = $wantarray ? [$org->($sth, @_)] : scalar $org->($sth, @_);
151 15         96 my $time = sprintf '%.6f', tv_interval $begin, [gettimeofday];
152              
153 15         413 $class->_logging($dbh, $ret, $time, \@params, $explain);
154              
155 15 50       2064 return $wantarray ? @$res : $res;
156 22         187 };
157             }
158              
159             sub _st_bind_param {
160 22     22   36 my ($class, $org) = @_;
161              
162             return sub {
163 6     6   7567 my ($sth, $p_num, $value, $attr) = @_;
164 6   100     110 $sth->{private_DBIx_QueryLog_params} ||= [];
165 6   100     52 $sth->{private_DBIx_QueryLog_attrs } ||= [];
166 6 50 50     41 $attr = +{ TYPE => $attr || 0 } unless ref $attr eq 'HASH';
167 6         29 $sth->{private_DBIx_QueryLog_params}[$p_num - 1] = $value;
168 6         24 $sth->{private_DBIx_QueryLog_attrs }[$p_num - 1] = $attr;
169 6         32 $org->(@_);
170 22         105 };
171             }
172              
173             sub _select_array {
174 66     66   100 my ($class, $org, $is_selectrow_array) = @_;
175              
176             return sub {
177 12     12   18917 my $wantarray = wantarray;
178 12         28 my ($dbh, $stmt, $attr, @bind) = @_;
179              
180 23     23   123 no warnings qw(redefine prototype);
  23         33  
  23         15506  
181 12         28 local *DBI::st::execute = _ORG_EXECUTE; # suppress duplicate logging
182              
183 12   33     66 my $probability = $container->{probability} || $ENV{DBIX_QUERYLOG_PROBABILITY};
184 12 50 33     109 if ($probability && int(rand() * $probability) % $probability != 0) {
185 0         0 return $org->($dbh, $stmt, $attr, @bind);
186             }
187              
188 12 50       29 my $ret = ref $stmt ? $stmt->{Statement} : $stmt;
189              
190 12         13 my $explain;
191 12 100 100     57 if ($container->{explain} || $ENV{DBIX_QUERYLOG_EXPLAIN}) {
192 6         13 $explain = _explain($dbh, $ret, \@bind);
193             }
194              
195 12 100 66     71 unless (($container->{skip_bind} || $ENV{DBIX_QUERYLOG_SKIP_BIND}) && @bind) {
      66        
196 11         31 $ret = _bind($dbh, $ret, \@bind);
197             }
198              
199 12         51 my $begin = [gettimeofday];
200 12         16 my $res;
201 12 100       28 if ($is_selectrow_array) {
202 5 50       47 $res = $wantarray ? [$org->($dbh, $stmt, $attr, @bind)] : $org->($dbh, $stmt, $attr, @bind);
203             }
204             else {
205 7         49 $res = $org->($dbh, $stmt, $attr, @bind);
206             }
207 12         1797 my $time = sprintf '%.6f', tv_interval $begin, [gettimeofday];
208              
209 12         280 $class->_logging($dbh, $ret, $time, \@bind, $explain);
210              
211 12 100       5888 if ($is_selectrow_array) {
212 5 50       60 return $wantarray ? @$res : $res;
213             }
214 7         83 return $res;
215 66         290 };
216             }
217              
218             sub _db_do {
219 22     22   41 my ($class, $org) = @_;
220              
221             return sub {
222 330 50   330   179004 my $wantarray = wantarray ? 1 : 0;
223 330         601 my ($dbh, $stmt, $attr, @bind) = @_;
224              
225 330 100 33     9089 if ($dbh->{Driver}{Name} ne 'mysql' && $dbh->{Driver}{Name} ne 'Pg' && !($dbh->{Driver}{Name} eq 'SQLite' && _HAS_SQLITE && !@bind)) {
      66        
      66        
226 8         45 return $org->($dbh, $stmt, $attr, @bind);
227             }
228              
229 322   100     1886 my $probability = $container->{probability} || $ENV{DBIX_QUERYLOG_PROBABILITY};
230 322 100 100     1310 if ($probability && int(rand() * $probability) % $probability != 0) {
231 176         667 return $org->($dbh, $stmt, $attr, @bind);
232             }
233              
234 146         251 my $ret = $stmt;
235              
236 146         140 my $explain;
237 146 100 100     612 if ($container->{explain} || $ENV{DBIX_QUERYLOG_EXPLAIN}) {
238 6         12 $explain = _explain($dbh, $ret, \@bind);
239             }
240              
241 146 50 33     672 unless (($container->{skip_bind} || $ENV{DBIX_QUERYLOG_SKIP_BIND}) && @bind) {
      33        
242 146         340 $ret = _bind($dbh, $ret, \@bind);
243             }
244              
245 146         537 my $begin = [gettimeofday];
246 146 50       717 my $res = $wantarray ? [$org->($dbh, $stmt, $attr, @bind)] : scalar $org->($dbh, $stmt, $attr, @bind);
247 146         14407 my $time = sprintf '%.6f', tv_interval $begin, [gettimeofday];
248              
249 146         3216 $class->_logging($dbh, $ret, $time, \@bind, $explain);
250              
251 146 50       2786 return $wantarray ? @$res : $res;
252 22         118 };
253             }
254              
255             sub _explain {
256 14     14   22 my ($dbh, $ret, $params, $types) = @_;
257 14   100     49 $types ||= [];
258              
259 14 50       85 return unless $ret =~ m|
260             \A # at start of string
261             (?:
262             \s* # white space
263             (?: /\* .*? \*/ )* # /* ... */
264             \s* # while space
265             )*
266             SELECT
267             \s* # white space
268             .+? # columns
269             \s* # white space
270             FROM
271             \s* # white space
272             |ixms;
273              
274 23     23   130 no warnings qw(redefine prototype);
  23         35  
  23         30917  
275 14         22 local *DBI::st::execute = _ORG_EXECUTE; # suppress duplicate logging
276              
277 14         9 my $sth;
278 14 50 33     257 if ($dbh->{Driver}{Name} eq 'mysql' || $dbh->{Driver}{Name} eq 'Pg') {
    50          
279 0         0 my $sql = 'EXPLAIN ' . _bind($dbh, $ret, $params, $types);
280 0         0 $sth = $dbh->prepare($sql);
281 0         0 $sth->execute;
282             } elsif ($dbh->{Driver}{Name} eq 'SQLite') {
283 14         25 my $sql = 'EXPLAIN QUERY PLAN ' . _bind($dbh, $ret, $params, $types);
284 14         61 $sth = $dbh->prepare($sql);
285 14         864 $sth->execute;
286             } else {
287             # not supported
288 0         0 return;
289             }
290              
291             return sub {
292 14     14   29 my %args = @_;
293              
294 14 100 66     84 return $sth->fetchall_arrayref(+{}) unless defined $args{print} and $args{print};
295              
296 10         47 my $t = Text::ASCIITable->new();
297 10         265 $t->setCols(@{$sth->{NAME}});
  10         123  
298 10 50       861 $t->addRow(map { defined($_) ? $_ : 'NULL' } @$_) for @{$sth->fetchall_arrayref};
  10         153  
  40         75  
299              
300 10         1687 return $t;
301 14         105 };
302             }
303              
304             sub _bind {
305 180     180   233 my ($dbh, $ret, $params, $types) = @_;
306 180   100     580 $types ||= [];
307 180         184 my $i = 0;
308 180 50 33     2174 if ($dbh->{Driver}{Name} eq 'mysql' or $dbh->{Driver}{Name} eq 'Pg') {
309 0         0 my $limit_flag = 0;
310 0         0 $ret =~ s{([?)])}{
311 0 0       0 if ($1 eq '?') {
    0          
312 0   0     0 $limit_flag ||= do {
313 0         0 my $pos = pos $ret;
314 0 0 0     0 ($pos >= 6 && substr($ret, $pos - 6, 6) =~ /\A[Ll](?:IMIT|imit) \z/) ? 1 : 0;
315             };
316 0 0       0 if ($limit_flag) {
317 0         0 $params->[$i++]
318             }
319             else {
320 0         0 my $type = $types->[$i];
321 0 0 0     0 if (defined $type and $dbh->{Driver}{Name} eq 'Pg' and $type == 0) {
      0        
322 0         0 $type = undef;
323             }
324 0 0       0 $dbh->quote($params->[$i++], defined $type ? $type : ());
325             }
326             }
327             elsif ($1 eq ')') {
328 0         0 $limit_flag = 0;
329 0         0 ')';
330             }
331             }eg;
332             }
333             else {
334 180         423 $ret =~ s/\?/$dbh->quote($params->[$i], $types->[$i++])/eg;
  16         159  
335             }
336 180         635 return $ret;
337             }
338              
339             sub _logging {
340 173     173   346 my ($class, $dbh, $ret, $time, $bind_params, $explain) = @_;
341              
342 173   100     867 my $threshold = $container->{threshold} || $ENV{DBIX_QUERYLOG_THRESHOLD};
343 173 100 66     438 return unless !$threshold || $time > $threshold;
344              
345 171   50     310 $bind_params ||= [];
346              
347 171         206 my $i = 0;
348 171         572 my $caller = { pkg => '???', line => '???', file => '???' };
349 171         1533 while (my @c = caller(++$i)) {
350 187 100 66     809 if (!$SKIP_PKG_MAP{$c[0]} and $c[0] !~ /^DB[DI]::/) {
351 171         505 $caller = { pkg => $c[0], file => $c[1], line => $c[2] };
352 171         627 last;
353             }
354             }
355              
356 171         227 my $sql = $ret;
357 171 100 100     729 if ($container->{skip_bind} || $ENV{DBIX_QUERYLOG_SKIP_BIND}) {
358 7         9 local $" = ', ';
359 7 50       24 $ret .= " : [@$bind_params]" if @$bind_params;
360             }
361              
362 171 100 66     643 if ($container->{compact} || $ENV{DBIX_QUERYLOG_COMPACT}) {
363 2         6 my ($buff, $i) = ('', 0);
364 2         9 my $skip_space = 0;
365 2         8 my $before_escape = 0;
366 2         4 my $quote_char = '';
367 2         14 for (my ($i, $l) = (0, length $ret); $i < $l; ++$i) {
368 284         256 my $s = substr $ret, $i, 1;
369 284 100 100     2319 if (!$quote_char && ($s eq q{ }||$s eq "\n"||$s eq "\t"||$s eq "\r")) {
    100 66        
    100 100        
    100 66        
      100        
      100        
370 97 100       165 next if $skip_space;
371 46         42 $buff .= q{ };
372 46         35 $skip_space = 1;
373 46         68 next;
374             }
375             elsif ($s eq q{'} || $s eq q{"} || $s eq q{`}) {
376 20 100 100     44 unless ($quote_char) {
    100          
377 8         10 $quote_char = $s;
378             }
379             elsif (!$before_escape && $s eq $quote_char) {
380 7         8 $quote_char = '';
381             }
382             else {
383 5         6 $before_escape = 0;
384             }
385             }
386             elsif (!$before_escape && $quote_char && $s eq q{\\}) {
387 1         1 $before_escape = 1;
388             }
389             elsif (!$quote_char) {
390 133 100 100     565 if ($s eq q{(}) {
    100 66        
391 2         1 $buff .= $s;
392 2         2 $skip_space = 1;
393 2         3 next;
394             }
395             elsif (($s eq q{)}||$s eq q{,}) && substr($buff, -1, 1) eq q{ }) {
396 4         4 substr($buff, -1, 1) = '';
397             }
398             }
399 185         141 $buff .= $s;
400 185         273 $skip_space = 0;
401             }
402 2         34 ($ret = $buff) =~ s/^\s|\s$//g;
403             }
404              
405 171 100 66     659 if ($container->{useqq} || $ENV{DBIX_QUERYLOG_USEQQ}) {
406 2         6 local $Data::Dumper::Useqq = 1;
407 2         4 local $Data::Dumper::Terse = 1;
408 2         7 local $Data::Dumper::Indent = 0;
409 2         14 $ret = Data::Dumper::Dumper($ret);
410             }
411              
412 171   66     743 my $color = $container->{color} || $ENV{DBIX_QUERYLOG_COLOR};
413 171         197 my $localtime = do {
414 171         4631 my ($sec, $min, $hour, $day, $mon, $year) = localtime;
415 171         958 sprintf '%d-%02d-%02dT%02d:%02d:%02d', $year + 1900, $mon + 1, $day, $hour, $min, $sec;
416             };
417 171         2147 my $data_source = "$dbh->{Driver}{Name}:$dbh->{Name}";
418 171 100 100     1694 my $message = sprintf "[%s] [%s] [%s] %s%s at %s line %s\n",
    100          
419             $localtime, $caller->{pkg}, $time,
420             $container->{show_data_source} || $ENV{DBIX_QUERYLOG_SHOW_DATASOURCE} ? "[$data_source] " : '',
421             $color ? colored([$color], $ret) : $ret,
422             $caller->{file}, $caller->{line};
423              
424 171 100       401 if (my $logger = $container->{logger}) {
425 3 100       15 my %explain = $explain ? (explain => $explain->()) : ();
426 3         197 $logger->log(
427             level => $LOG_LEVEL,
428             message => $message,
429             params => {
430             dbh => $dbh,
431             localtime => $localtime,
432             time => $time,
433             sql => $sql,
434             bind_params => $bind_params,
435             data_source => $data_source,
436             %explain,
437             %$caller,
438             },
439             );
440             }
441             else {
442 168 100       390 if (ref $OUTPUT eq 'CODE') {
443 11 100       27 my %explain = $explain ? (explain => $explain->()) : ();
444 11         171 $OUTPUT->(
445             dbh => $dbh,
446             level => $LOG_LEVEL,
447             message => $message,
448             localtime => $localtime,
449             time => $time,
450             sql => $sql,
451             bind_params => $bind_params,
452             data_source => $data_source,
453             %explain,
454             %$caller,
455             );
456             }
457             else {
458 157 100       164 print {$OUTPUT} $message, $explain ? $explain->(print => 1) : ();
  157         1069  
459             }
460             }
461             }
462              
463             {
464             package # hide from pause
465             DBIx::QueryLog::Guard;
466             sub new {
467 2     2   3 my ($class, $org_is_enabled) = @_;
468 2         7 bless [$org_is_enabled], shift;
469             }
470             sub DESTROY {
471 2 100   2   2972 if (shift->[0]) {
472 1         5 DBIx::QueryLog->enable();
473             }
474             else {
475 1         5 DBIx::QueryLog->disable();
476             }
477             }
478             }
479              
480             1;
481             __END__