File Coverage

blib/lib/DBIx/QueryLog.pm
Criterion Covered Total %
statement 278 300 92.6
branch 95 132 71.9
condition 96 156 61.5
subroutine 44 44 100.0
pod 3 3 100.0
total 516 635 81.2


line stmt bran cond sub pod time code
1             package DBIx::QueryLog;
2              
3 23     23   2308539 use strict;
  23         53  
  23         643  
4 23     23   109 use warnings;
  23         49  
  23         576  
5 23     23   515 use 5.008_001;
  23         85  
6              
7 23     23   7269 use DBI;
  23         68815  
  23         1214  
8 23     23   3102 use Time::HiRes qw(gettimeofday tv_interval);
  23         4590  
  23         167  
9 23     23   25193 use Term::ANSIColor qw(colored);
  23         186644  
  23         11603  
10 23     23   21921 use Text::ASCIITable;
  23         426672  
  23         1090  
11 23     23   3015 use Data::Dumper ();
  23         18031  
  23         1275  
12              
13             $ENV{ANSI_COLORS_DISABLED} = 1 if $^O eq 'MSWin32';
14              
15             our $VERSION = '0.41';
16              
17 23     23   120 use constant _ORG_EXECUTE => \&DBI::st::execute;
  23         44  
  23         1558  
18 23     23   315 use constant _ORG_BIND_PARAM => \&DBI::st::bind_param;
  23         47  
  23         1189  
19 23     23   102 use constant _ORG_DB_DO => \&DBI::db::do;
  23         44  
  23         1048  
20 23     23   101 use constant _ORG_DB_SELECTALL_ARRAYREF => \&DBI::db::selectall_arrayref;
  23         39  
  23         1052  
21 23     23   112 use constant _ORG_DB_SELECTROW_ARRAYREF => \&DBI::db::selectrow_arrayref;
  23         41  
  23         1129  
22 23     23   108 use constant _ORG_DB_SELECTROW_ARRAY => \&DBI::db::selectrow_array;
  23         36  
  23         1479  
23              
24 23 50   23   122 use constant _HAS_MYSQL => eval { require DBD::mysql; 1 } ? 1 : 0;
  23         47  
  23         57  
  23         9791  
  0         0  
25 23 50   23   111 use constant _HAS_PG => eval { require DBD::Pg; 1 } ? 1 : 0;
  23         35  
  23         40  
  23         9016  
  0         0  
26 23 50   23   134 use constant _HAS_SQLITE => eval { require DBD::SQLite; DBD::SQLite->VERSION(1.48); 1 } ? 1 : 0;
  23         33  
  23         52  
  23         3284  
  23         29741  
  23         1470  
27 23 50   23   147 use constant _PP_MODE => $INC{'DBI/PurePerl.pm'} ? 1 : 0;
  23         41  
  23         4859  
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   10144 my ($class) = @_;
46              
47 24   66     213 $st_execute ||= $class->_st_execute(_ORG_EXECUTE);
48 24   66     165 $st_bind_param ||= $class->_st_bind_param(_ORG_BIND_PARAM);
49 24   66     231 $db_do ||= $class->_db_do(_ORG_DB_DO) if _HAS_MYSQL or _HAS_PG or _HAS_SQLITE;
50 24         38 unless (_PP_MODE) {
51 24   66     147 $selectall_arrayref ||= $class->_select_array(_ORG_DB_SELECTALL_ARRAYREF);
52 24   66     141 $selectrow_arrayref ||= $class->_select_array(_ORG_DB_SELECTROW_ARRAYREF);
53 24   66     140 $selectrow_array ||= $class->_select_array(_ORG_DB_SELECTROW_ARRAY, 1);
54             }
55              
56 23     23   106 no warnings qw(redefine prototype);
  23         37  
  23         3269  
57 24         91 *DBI::st::execute = $st_execute;
58 24         62 *DBI::st::bind_param = $st_bind_param;
59 24         73 *DBI::db::do = $db_do if _HAS_MYSQL or _HAS_PG or _HAS_SQLITE;
60 24         39 unless (_PP_MODE) {
61 24         60 *DBI::db::selectall_arrayref = $selectall_arrayref;
62 24         55 *DBI::db::selectrow_arrayref = $selectrow_arrayref;
63 24         53 *DBI::db::selectrow_array = $selectrow_array;
64             }
65              
66 24         30169 $is_enabled = 1;
67             }
68              
69             sub unimport {
70 23     23   106 no warnings qw(redefine prototype);
  23         44  
  23         6089  
71 5     5   41783 *DBI::st::execute = _ORG_EXECUTE;
72 5         19 *DBI::st::bind_param = _ORG_BIND_PARAM;
73 5         17 *DBI::db::do = _ORG_DB_DO if _HAS_MYSQL or _HAS_PG or _HAS_SQLITE;
74 5         10 unless (_PP_MODE) {
75 5         18 *DBI::db::selectall_arrayref = _ORG_DB_SELECTALL_ARRAYREF;
76 5         13 *DBI::db::selectrow_arrayref = _ORG_DB_SELECTROW_ARRAYREF;
77 5         14 *DBI::db::selectrow_array = _ORG_DB_SELECTROW_ARRAY;
78             }
79              
80 5         19 $is_enabled = 0;
81             }
82              
83             *enable = *begin = \&import;
84             *disable = *end = \&unimport;
85              
86             sub guard {
87 1     1 1 5651 my $org_is_enabled = DBIx::QueryLog->is_enabled;
88 1         8 DBIx::QueryLog->enable();
89 1         13 return DBIx::QueryLog::Guard->new($org_is_enabled);
90             }
91              
92             sub ignore_trace {
93 1     1 1 9 my $org_is_enabled = DBIx::QueryLog->is_enabled;
94 1         4 DBIx::QueryLog->disable();
95 1         7 return DBIx::QueryLog::Guard->new($org_is_enabled);
96             }
97              
98 2     2 1 8 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   167 no strict 'refs';
  23         37  
  23         1162  
106             *{__PACKAGE__."::$accessor"} = sub {
107 23     23   128 use strict 'refs';
  23         35  
  23         14378  
108 53     53   971561 my ($class, $args) = @_;
109 53 100       244 return $container->{$accessor} unless @_ > 1;
110 36         191 $container->{$accessor} = $args;
111             };
112             }
113              
114             sub _st_execute {
115 22     22   54 my ($class, $org) = @_;
116              
117             return sub {
118 15 50   15   5680 my $wantarray = wantarray ? 1 : 0;
119 15         67 my $sth = shift;
120 15         42 my @params = @_;
121 15         27 my @types;
122              
123 15   33     91 my $probability = $container->{probability} || $ENV{DBIX_QUERYLOG_PROBABILITY};
124 15 50 33     66 if ($probability && int(rand() * $probability) % $probability != 0) {
125 0         0 return $org->($sth, @params);
126             }
127              
128 15         111 my $dbh = $sth->{Database};
129 15         99 my $ret = $sth->{Statement};
130 15 100       148 if (my $attrs = $sth->{private_DBIx_QueryLog_attrs}) {
131 5         30 my $bind_params = $sth->{private_DBIx_QueryLog_params};
132 5         28 for my $i (1..@$attrs) {
133 6         23 push @types, $attrs->[$i - 1]{TYPE};
134 6 50       32 push @params, $bind_params->[$i - 1] if $bind_params;
135             }
136             }
137             # DBD::Pg::st warns "undef in subroutine"
138 15 50       317 $sth->{private_DBIx_QueryLog_params} = $dbh->{Driver}{Name} eq 'Pg' ? '' : undef;
139              
140 15         54 my $explain;
141 15 100 66     158 if ($container->{explain} || $ENV{DBIX_QUERYLOG_EXPLAIN}) {
142 2         7 $explain = _explain($dbh, $ret, \@params, \@types);
143             }
144              
145 15 100 66     156 unless (($container->{skip_bind} || $ENV{DBIX_QUERYLOG_SKIP_BIND}) && @params) {
      66        
146 9         48 $ret = _bind($dbh, $ret, \@params, \@types);
147             }
148              
149 15         116 my $begin = [gettimeofday];
150 15 50       1137 my $res = $wantarray ? [$org->($sth, @_)] : scalar $org->($sth, @_);
151 15         125 my $time = sprintf '%.6f', tv_interval $begin, [gettimeofday];
152              
153 15         476 $class->_logging($dbh, $ret, $time, \@params, $explain);
154              
155 15 50       2928 return $wantarray ? @$res : $res;
156 22         198 };
157             }
158              
159             sub _st_bind_param {
160 22     22   53 my ($class, $org) = @_;
161              
162             return sub {
163 6     6   14581 my ($sth, $p_num, $value, $attr) = @_;
164 6   100     152 $sth->{private_DBIx_QueryLog_params} ||= [];
165 6   100     85 $sth->{private_DBIx_QueryLog_attrs } ||= [];
166 6 50 50     64 $attr = +{ TYPE => $attr || 0 } unless ref $attr eq 'HASH';
167 6         37 $sth->{private_DBIx_QueryLog_params}[$p_num - 1] = $value;
168 6         34 $sth->{private_DBIx_QueryLog_attrs }[$p_num - 1] = $attr;
169 6         61 $org->(@_);
170 22         139 };
171             }
172              
173             sub _select_array {
174 66     66   119 my ($class, $org, $is_selectrow_array) = @_;
175              
176             return sub {
177 12     12   21220 my $wantarray = wantarray;
178 12         31 my ($dbh, $stmt, $attr, @bind) = @_;
179              
180 23     23   118 no warnings qw(redefine prototype);
  23         40  
  23         17098  
181 12         43 local *DBI::st::execute = _ORG_EXECUTE; # suppress duplicate logging
182              
183 12   33     59 my $probability = $container->{probability} || $ENV{DBIX_QUERYLOG_PROBABILITY};
184 12 50 33     77 if ($probability && int(rand() * $probability) % $probability != 0) {
185 0         0 return $org->($dbh, $stmt, $attr, @bind);
186             }
187              
188 12 50       34 my $ret = ref $stmt ? $stmt->{Statement} : $stmt;
189              
190 12         20 my $explain;
191 12 100 66     59 if ($container->{explain} || $ENV{DBIX_QUERYLOG_EXPLAIN}) {
192 6         13 $explain = _explain($dbh, $ret, \@bind);
193             }
194              
195 12 100 66     68 unless (($container->{skip_bind} || $ENV{DBIX_QUERYLOG_SKIP_BIND}) && @bind) {
      66        
196 11         33 $ret = _bind($dbh, $ret, \@bind);
197             }
198              
199 12         53 my $begin = [gettimeofday];
200 12         20 my $res;
201 12 100       30 if ($is_selectrow_array) {
202 5 50       55 $res = $wantarray ? [$org->($dbh, $stmt, $attr, @bind)] : $org->($dbh, $stmt, $attr, @bind);
203             }
204             else {
205 7         59 $res = $org->($dbh, $stmt, $attr, @bind);
206             }
207 12         2024 my $time = sprintf '%.6f', tv_interval $begin, [gettimeofday];
208              
209 12         279 $class->_logging($dbh, $ret, $time, \@bind, $explain);
210              
211 12 100       8023 if ($is_selectrow_array) {
212 5 50       56 return $wantarray ? @$res : $res;
213             }
214 7         84 return $res;
215 66         450 };
216             }
217              
218             sub _db_do {
219 22     22   50 my ($class, $org) = @_;
220              
221             return sub {
222 332 50   332   114234 my $wantarray = wantarray ? 1 : 0;
223 332         723 my ($dbh, $stmt, $attr, @bind) = @_;
224              
225 332 100 33     10733 if ($dbh->{Driver}{Name} ne 'mysql' && $dbh->{Driver}{Name} ne 'Pg' && !($dbh->{Driver}{Name} eq 'SQLite' && _HAS_SQLITE && !@bind)) {
      66        
      66        
226 8         74 return $org->($dbh, $stmt, $attr, @bind);
227             }
228              
229 324   66     1757 my $probability = $container->{probability} || $ENV{DBIX_QUERYLOG_PROBABILITY};
230 324 100 100     1447 if ($probability && int(rand() * $probability) % $probability != 0) {
231 180         709 return $org->($dbh, $stmt, $attr, @bind);
232             }
233              
234 144         212 my $ret = $stmt;
235              
236 144         184 my $explain;
237 144 100 66     604 if ($container->{explain} || $ENV{DBIX_QUERYLOG_EXPLAIN}) {
238 8         19 $explain = _explain($dbh, $ret, \@bind);
239             }
240              
241 144 50 33     1031 unless (($container->{skip_bind} || $ENV{DBIX_QUERYLOG_SKIP_BIND}) && @bind) {
      33        
242 144         388 $ret = _bind($dbh, $ret, \@bind);
243             }
244              
245 144         615 my $begin = [gettimeofday];
246 144 50       808 my $res = $wantarray ? [$org->($dbh, $stmt, $attr, @bind)] : scalar $org->($dbh, $stmt, $attr, @bind);
247 142         15952 my $time = sprintf '%.6f', tv_interval $begin, [gettimeofday];
248              
249 142         6408 $class->_logging($dbh, $ret, $time, \@bind, $explain);
250              
251 142 50       3633 return $wantarray ? @$res : $res;
252 22         134 };
253             }
254              
255             sub _explain {
256 16     16   27 my ($dbh, $ret, $params, $types) = @_;
257 16   100     59 $types ||= [];
258              
259 16 100       91 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   123 no warnings qw(redefine prototype);
  23         54  
  23         34828  
275 14         29 local *DBI::st::execute = _ORG_EXECUTE; # suppress duplicate logging
276              
277 14         17 my $sth;
278 14 50 33     315 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         33 my $sql = 'EXPLAIN QUERY PLAN ' . _bind($dbh, $ret, $params, $types);
284 14         61 $sth = $dbh->prepare($sql);
285 14         926 $sth->execute;
286             } else {
287             # not supported
288 0         0 return;
289             }
290              
291 14 50       102 return if $DBI::err; # skip if maybe statement error
292             return sub {
293 14     14   33 my %args = @_;
294              
295 14 100 66     79 return $sth->fetchall_arrayref(+{}) unless defined $args{print} and $args{print};
296              
297 10         56 my $t = Text::ASCIITable->new();
298 10         276 $t->setCols(@{$sth->{NAME}});
  10         148  
299 10 50       804 $t->addRow(map { defined($_) ? $_ : 'NULL' } @$_) for @{$sth->fetchall_arrayref};
  10         143  
  40         99  
300              
301 10         2220 return $t;
302 14         77 };
303             }
304              
305             sub _bind {
306 178     178   328 my ($dbh, $ret, $params, $types) = @_;
307 178   100     729 $types ||= [];
308 178         263 my $i = 0;
309 178 50 33     3139 if ($dbh->{Driver}{Name} eq 'mysql' or $dbh->{Driver}{Name} eq 'Pg') {
310 0         0 my $limit_flag = 0;
311 0         0 $ret =~ s{([?)])}{
312 0 0       0 if ($1 eq '?') {
    0          
313 0   0     0 $limit_flag ||= do {
314 0         0 my $pos = pos $ret;
315 0 0 0     0 ($pos >= 6 && substr($ret, $pos - 6, 6) =~ /\A[Ll](?:IMIT|imit) \z/) ? 1 : 0;
316             };
317 0 0       0 if ($limit_flag) {
318 0         0 $params->[$i++]
319             }
320             else {
321 0         0 my $type = $types->[$i];
322 0 0 0     0 if (defined $type and $dbh->{Driver}{Name} eq 'Pg' and $type == 0) {
      0        
323 0         0 $type = undef;
324             }
325 0 0       0 $dbh->quote($params->[$i++], defined $type ? $type : ());
326             }
327             }
328             elsif ($1 eq ')') {
329 0         0 $limit_flag = 0;
330 0         0 ')';
331             }
332             }eg;
333             }
334             else {
335 178         488 $ret =~ s/\?/$dbh->quote($params->[$i], $types->[$i++])/eg;
  16         189  
336             }
337 178         883 return $ret;
338             }
339              
340             sub _logging {
341 169     169   380 my ($class, $dbh, $ret, $time, $bind_params, $explain) = @_;
342              
343 169   66     757 my $threshold = $container->{threshold} || $ENV{DBIX_QUERYLOG_THRESHOLD};
344 169 100 66     546 return unless !$threshold || $time > $threshold;
345              
346 167   50     474 $bind_params ||= [];
347              
348 167         252 my $i = 0;
349 167         861 my $caller = { pkg => '???', line => '???', file => '???' };
350 167         1741 while (my @c = caller(++$i)) {
351 183 100 66     1068 if (!$SKIP_PKG_MAP{$c[0]} and $c[0] !~ /^DB[DI]::/) {
352 167         638 $caller = { pkg => $c[0], file => $c[1], line => $c[2] };
353 167         776 last;
354             }
355             }
356              
357 167         360 my $sql = $ret;
358 167 100 66     722 if ($container->{skip_bind} || $ENV{DBIX_QUERYLOG_SKIP_BIND}) {
359 7         19 local $" = ', ';
360 7 50       48 $ret .= " : [@$bind_params]" if @$bind_params;
361             }
362              
363 167 100 66     701 if ($container->{compact} || $ENV{DBIX_QUERYLOG_COMPACT}) {
364 2         5 my ($buff, $i) = ('', 0);
365 2         4 my $skip_space = 0;
366 2         3 my $before_escape = 0;
367 2         4 my $quote_char = '';
368 2         8 for (my ($i, $l) = (0, length $ret); $i < $l; ++$i) {
369 284         332 my $s = substr $ret, $i, 1;
370 284 100 100     2777 if (!$quote_char && ($s eq q{ }||$s eq "\n"||$s eq "\t"||$s eq "\r")) {
    100 66        
    100 100        
    100 66        
      100        
      100        
371 97 100       197 next if $skip_space;
372 46         57 $buff .= q{ };
373 46         41 $skip_space = 1;
374 46         90 next;
375             }
376             elsif ($s eq q{'} || $s eq q{"} || $s eq q{`}) {
377 20 100 100     67 unless ($quote_char) {
    100          
378 8         11 $quote_char = $s;
379             }
380             elsif (!$before_escape && $s eq $quote_char) {
381 7         14 $quote_char = '';
382             }
383             else {
384 5         6 $before_escape = 0;
385             }
386             }
387             elsif (!$before_escape && $quote_char && $s eq q{\\}) {
388 1         1 $before_escape = 1;
389             }
390             elsif (!$quote_char) {
391 133 100 100     711 if ($s eq q{(}) {
    100 66        
392 2         2 $buff .= $s;
393 2         3 $skip_space = 1;
394 2         5 next;
395             }
396             elsif (($s eq q{)}||$s eq q{,}) && substr($buff, -1, 1) eq q{ }) {
397 4         5 substr($buff, -1, 1) = '';
398             }
399             }
400 185         207 $buff .= $s;
401 185         362 $skip_space = 0;
402             }
403 2         37 ($ret = $buff) =~ s/^\s|\s$//g;
404             }
405              
406 167 100 66     789 if ($container->{useqq} || $ENV{DBIX_QUERYLOG_USEQQ}) {
407 2         6 local $Data::Dumper::Useqq = 1;
408 2         4 local $Data::Dumper::Terse = 1;
409 2         6 local $Data::Dumper::Indent = 0;
410 2         23 $ret = Data::Dumper::Dumper($ret);
411             }
412              
413 167   66     687 my $color = $container->{color} || $ENV{DBIX_QUERYLOG_COLOR};
414 167         263 my $localtime = do {
415 167         5679 my ($sec, $min, $hour, $day, $mon, $year) = localtime;
416 167         1058 sprintf '%d-%02d-%02dT%02d:%02d:%02d', $year + 1900, $mon + 1, $day, $hour, $min, $sec;
417             };
418 167         2504 my $data_source = "$dbh->{Driver}{Name}:$dbh->{Name}";
419             my $message = sprintf "[%s] [%s] [%s] %s%s at %s line %s\n",
420             $localtime, $caller->{pkg}, $time,
421             $container->{show_data_source} || $ENV{DBIX_QUERYLOG_SHOW_DATASOURCE} ? "[$data_source] " : '',
422             $color ? colored([$color], $ret) : $ret,
423 167 100 66     1829 $caller->{file}, $caller->{line};
    100          
424              
425 167 100       589 if (my $logger = $container->{logger}) {
426 3 100       15 my %explain = $explain ? (explain => $explain->()) : ();
427 3         184 $logger->log(
428             level => $LOG_LEVEL,
429             message => $message,
430             params => {
431             dbh => $dbh,
432             localtime => $localtime,
433             time => $time,
434             sql => $sql,
435             bind_params => $bind_params,
436             data_source => $data_source,
437             %explain,
438             %$caller,
439             },
440             );
441             }
442             else {
443 164 100       478 if (ref $OUTPUT eq 'CODE') {
444 11 100       31 my %explain = $explain ? (explain => $explain->()) : ();
445 11         169 $OUTPUT->(
446             dbh => $dbh,
447             level => $LOG_LEVEL,
448             message => $message,
449             localtime => $localtime,
450             time => $time,
451             sql => $sql,
452             bind_params => $bind_params,
453             data_source => $data_source,
454             %explain,
455             %$caller,
456             );
457             }
458             else {
459 153 100       199 print {$OUTPUT} $message, $explain ? $explain->(print => 1) : ();
  153         1145  
460             }
461             }
462             }
463              
464             {
465             package # hide from pause
466             DBIx::QueryLog::Guard;
467             sub new {
468 2     2   7 my ($class, $org_is_enabled) = @_;
469 2         11 bless [$org_is_enabled], shift;
470             }
471             sub DESTROY {
472 2 100   2   3124 if (shift->[0]) {
473 1         6 DBIx::QueryLog->enable();
474             }
475             else {
476 1         8 DBIx::QueryLog->disable();
477             }
478             }
479             }
480              
481             1;
482             __END__