File Coverage

blib/lib/DBIx/QueryLog.pm
Criterion Covered Total %
statement 281 304 92.4
branch 99 136 72.7
condition 103 153 67.3
subroutine 44 44 100.0
pod 3 3 100.0
total 530 640 82.8


line stmt bran cond sub pod time code
1             package DBIx::QueryLog;
2              
3 23     23   2901087 use strict;
  23         257  
  23         696  
4 23     23   123 use warnings;
  23         44  
  23         627  
5 23     23   486 use 5.008_001;
  23         83  
6              
7 23     23   4892 use DBI;
  23         53055  
  23         1353  
8 23     23   1842 use Time::HiRes qw(gettimeofday tv_interval);
  23         3786  
  23         203  
9 23     23   17048 use Term::ANSIColor qw(colored);
  23         198602  
  23         39176  
10 23     23   14070 use Text::ASCIITable;
  23         179356  
  23         1222  
11 23     23   1940 use Data::Dumper ();
  23         19979  
  23         1331  
12              
13             $ENV{ANSI_COLORS_DISABLED} = 1 if $^O eq 'MSWin32';
14              
15             our $VERSION = '0.42';
16              
17 23     23   145 use constant _ORG_EXECUTE => \&DBI::st::execute;
  23         46  
  23         1787  
18 23     23   147 use constant _ORG_BIND_PARAM => \&DBI::st::bind_param;
  23         46  
  23         1279  
19 23     23   135 use constant _ORG_DB_DO => \&DBI::db::do;
  23         41  
  23         1318  
20 23     23   130 use constant _ORG_DB_SELECTALL_ARRAYREF => \&DBI::db::selectall_arrayref;
  23         45  
  23         1132  
21 23     23   121 use constant _ORG_DB_SELECTROW_ARRAYREF => \&DBI::db::selectrow_arrayref;
  23         55  
  23         1410  
22 23     23   149 use constant _ORG_DB_SELECTROW_ARRAY => \&DBI::db::selectrow_array;
  23         41  
  23         1936  
23              
24 23 50   23   176 use constant _HAS_MYSQL => eval { require DBD::mysql; 1 } ? 1 : 0;
  23         62  
  23         46  
  23         4809  
  0         0  
25 23 50   23   182 use constant _HAS_PG => eval { require DBD::Pg; 1 } ? 1 : 0;
  23         61  
  23         74  
  23         4397  
  0         0  
26 23 50   23   177 use constant _HAS_SQLITE => eval { require DBD::SQLite; DBD::SQLite->VERSION(1.48); 1 } ? 1 : 0;
  23         52  
  23         41  
  23         2337  
  23         33622  
  23         1725  
27 23 50   23   155 use constant _PP_MODE => $INC{'DBI/PurePerl.pm'} ? 1 : 0;
  23         53  
  23         5025  
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   11670 my ($class) = @_;
46              
47 24   66     165 $st_execute ||= $class->_st_execute(_ORG_EXECUTE);
48 24   66     131 $st_bind_param ||= $class->_st_bind_param(_ORG_BIND_PARAM);
49 24   66     128 $db_do ||= $class->_db_do(_ORG_DB_DO) if _HAS_MYSQL or _HAS_PG or _HAS_SQLITE;
50 24         35 unless (_PP_MODE) {
51 24   66     128 $selectall_arrayref ||= $class->_select_array(_ORG_DB_SELECTALL_ARRAYREF);
52 24   66     125 $selectrow_arrayref ||= $class->_select_array(_ORG_DB_SELECTROW_ARRAYREF);
53 24   66     197 $selectrow_array ||= $class->_select_array(_ORG_DB_SELECTROW_ARRAY, 1);
54             }
55              
56 23     23   169 no warnings qw(redefine prototype);
  23         45  
  23         3202  
57 24         112 *DBI::st::execute = $st_execute;
58 24         59 *DBI::st::bind_param = $st_bind_param;
59 24         69 *DBI::db::do = $db_do if _HAS_MYSQL or _HAS_PG or _HAS_SQLITE;
60 24         39 unless (_PP_MODE) {
61 24         100 *DBI::db::selectall_arrayref = $selectall_arrayref;
62 24         45 *DBI::db::selectrow_arrayref = $selectrow_arrayref;
63 24         48 *DBI::db::selectrow_array = $selectrow_array;
64             }
65              
66 24         25067 $is_enabled = 1;
67             }
68              
69             sub unimport {
70 23     23   171 no warnings qw(redefine prototype);
  23         44  
  23         6199  
71 5     5   45193 *DBI::st::execute = _ORG_EXECUTE;
72 5         15 *DBI::st::bind_param = _ORG_BIND_PARAM;
73 5         15 *DBI::db::do = _ORG_DB_DO if _HAS_MYSQL or _HAS_PG or _HAS_SQLITE;
74 5         10 unless (_PP_MODE) {
75 5         29 *DBI::db::selectall_arrayref = _ORG_DB_SELECTALL_ARRAYREF;
76 5         16 *DBI::db::selectrow_arrayref = _ORG_DB_SELECTROW_ARRAYREF;
77 5         12 *DBI::db::selectrow_array = _ORG_DB_SELECTROW_ARRAY;
78             }
79              
80 5         18 $is_enabled = 0;
81             }
82              
83             *enable = *begin = \&import;
84             *disable = *end = \&unimport;
85              
86             sub guard {
87 1     1 1 4881 my $org_is_enabled = DBIx::QueryLog->is_enabled;
88 1         5 DBIx::QueryLog->enable();
89 1         8 return DBIx::QueryLog::Guard->new($org_is_enabled);
90             }
91              
92             sub ignore_trace {
93 1     1 1 10 my $org_is_enabled = DBIx::QueryLog->is_enabled;
94 1         11 DBIx::QueryLog->disable();
95 1         7 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   633 no strict 'refs';
  23         66  
  23         1366  
106             *{__PACKAGE__."::$accessor"} = sub {
107 23     23   140 use strict 'refs';
  23         40  
  23         15211  
108 53     53   68202 my ($class, $args) = @_;
109 53 100       271 return $container->{$accessor} unless @_ > 1;
110 36         156 $container->{$accessor} = $args;
111             };
112             }
113              
114             sub _st_execute {
115 22     22   56 my ($class, $org) = @_;
116              
117             return sub {
118 17 50   17   8224 my $wantarray = wantarray ? 1 : 0;
119 17         46 my $sth = shift;
120 17         44 my @params = @_;
121 17         33 my @types;
122              
123 17   33     95 my $probability = $container->{probability} || $ENV{DBIX_QUERYLOG_PROBABILITY};
124 17 50 33     69 if ($probability && int(rand() * $probability) % $probability != 0) {
125 0         0 return $org->($sth, @params);
126             }
127              
128 17         137 my $dbh = $sth->{Database};
129 17         100 my $ret = $sth->{Statement};
130 17 100       130 if (my $attrs = $sth->{private_DBIx_QueryLog_attrs}) {
131 7         25 my $bind_params = $sth->{private_DBIx_QueryLog_params};
132 7         30 for my $i (1..@$attrs) {
133 10         28 push @types, $attrs->[$i - 1]{TYPE};
134 10 50       32 push @params, $bind_params->[$i - 1] if $bind_params;
135             }
136             }
137             # DBD::Pg::st warns "undef in subroutine"
138 17 50       271 $sth->{private_DBIx_QueryLog_params} = $dbh->{Driver}{Name} eq 'Pg' ? '' : undef;
139              
140 17         57 my $explain;
141 17 100 100     108 if ($container->{explain} || $ENV{DBIX_QUERYLOG_EXPLAIN}) {
142 2         9 $explain = _explain($dbh, $ret, \@params, \@types);
143             }
144              
145 17 100 100     118 unless (($container->{skip_bind} || $ENV{DBIX_QUERYLOG_SKIP_BIND}) && @params) {
      66        
146 9         43 $ret = _bind($dbh, $ret, \@params, \@types);
147             }
148              
149 17         117 my $begin = [gettimeofday];
150 17 50       988 my $res = $wantarray ? [$org->($sth, @_)] : scalar $org->($sth, @_);
151 17         167 my $time = sprintf '%.6f', tv_interval $begin, [gettimeofday];
152              
153 17         550 $class->_logging($dbh, $ret, $time, \@params, $explain);
154              
155 17 50       2454 return $wantarray ? @$res : $res;
156 22         206 };
157             }
158              
159             sub _st_bind_param {
160 22     22   59 my ($class, $org) = @_;
161              
162             return sub {
163 10     10   19483 my ($sth, $p_num, $value, $attr) = @_;
164 10   100     175 $sth->{private_DBIx_QueryLog_params} ||= [];
165 10   100     100 $sth->{private_DBIx_QueryLog_attrs } ||= [];
166 10 50 50     60 $attr = +{ TYPE => $attr || 0 } unless ref $attr eq 'HASH';
167 10         49 $sth->{private_DBIx_QueryLog_params}[$p_num - 1] = $value;
168 10         41 $sth->{private_DBIx_QueryLog_attrs }[$p_num - 1] = $attr;
169 10         60 $org->(@_);
170 22         142 };
171             }
172              
173             sub _select_array {
174 66     66   157 my ($class, $org, $is_selectrow_array) = @_;
175              
176             return sub {
177 12     12   34333 my $wantarray = wantarray;
178 12         43 my ($dbh, $stmt, $attr, @bind) = @_;
179              
180 23     23   174 no warnings qw(redefine prototype);
  23         50  
  23         17386  
181 12         46 local *DBI::st::execute = _ORG_EXECUTE; # suppress duplicate logging
182              
183 12   33     79 my $probability = $container->{probability} || $ENV{DBIX_QUERYLOG_PROBABILITY};
184 12 50 33     55 if ($probability && int(rand() * $probability) % $probability != 0) {
185 0         0 return $org->($dbh, $stmt, $attr, @bind);
186             }
187              
188 12 50       39 my $ret = ref $stmt ? $stmt->{Statement} : $stmt;
189              
190 12         20 my $explain;
191 12 100 100     66 if ($container->{explain} || $ENV{DBIX_QUERYLOG_EXPLAIN}) {
192 6         15 $explain = _explain($dbh, $ret, \@bind);
193             }
194              
195 12 100 66     81 unless (($container->{skip_bind} || $ENV{DBIX_QUERYLOG_SKIP_BIND}) && @bind) {
      66        
196 11         41 $ret = _bind($dbh, $ret, \@bind);
197             }
198              
199 12         80 my $begin = [gettimeofday];
200 12         22 my $res;
201 12 100       34 if ($is_selectrow_array) {
202 5 50       67 $res = $wantarray ? [$org->($dbh, $stmt, $attr, @bind)] : $org->($dbh, $stmt, $attr, @bind);
203             }
204             else {
205 7         98 $res = $org->($dbh, $stmt, $attr, @bind);
206             }
207 12         2197 my $time = sprintf '%.6f', tv_interval $begin, [gettimeofday];
208              
209 12         351 $class->_logging($dbh, $ret, $time, \@bind, $explain);
210              
211 12 100       6679 if ($is_selectrow_array) {
212 5 50       72 return $wantarray ? @$res : $res;
213             }
214 7         102 return $res;
215 66         617 };
216             }
217              
218             sub _db_do {
219 22     22   56 my ($class, $org) = @_;
220              
221             return sub {
222 332 50   332   102205 my $wantarray = wantarray ? 1 : 0;
223 332         757 my ($dbh, $stmt, $attr, @bind) = @_;
224              
225 332 100 33     7499 if ($dbh->{Driver}{Name} ne 'mysql' && $dbh->{Driver}{Name} ne 'Pg' && !($dbh->{Driver}{Name} eq 'SQLite' && _HAS_SQLITE && !@bind)) {
      66        
      66        
226 8         65 return $org->($dbh, $stmt, $attr, @bind);
227             }
228              
229 324   100     1691 my $probability = $container->{probability} || $ENV{DBIX_QUERYLOG_PROBABILITY};
230 324 100 100     1284 if ($probability && int(rand() * $probability) % $probability != 0) {
231 176         656 return $org->($dbh, $stmt, $attr, @bind);
232             }
233              
234 148         213 my $ret = $stmt;
235              
236 148         213 my $explain;
237 148 100 100     542 if ($container->{explain} || $ENV{DBIX_QUERYLOG_EXPLAIN}) {
238 8         22 $explain = _explain($dbh, $ret, \@bind);
239             }
240              
241 148 50 33     639 unless (($container->{skip_bind} || $ENV{DBIX_QUERYLOG_SKIP_BIND}) && @bind) {
      33        
242 148         358 $ret = _bind($dbh, $ret, \@bind);
243             }
244              
245 148         612 my $begin = [gettimeofday];
246 148 50       693 my $res = $wantarray ? [$org->($dbh, $stmt, $attr, @bind)] : scalar $org->($dbh, $stmt, $attr, @bind);
247 146         14952 my $time = sprintf '%.6f', tv_interval $begin, [gettimeofday];
248              
249 146         3972 $class->_logging($dbh, $ret, $time, \@bind, $explain);
250              
251 146 50       3143 return $wantarray ? @$res : $res;
252 22         128 };
253             }
254              
255             sub _explain {
256 16     16   37 my ($dbh, $ret, $params, $types) = @_;
257 16   100     59 $types ||= [];
258              
259 16 100       105 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   187 no warnings qw(redefine prototype);
  23         51  
  23         35110  
275 14         36 local *DBI::st::execute = _ORG_EXECUTE; # suppress duplicate logging
276              
277 14         17 my $sth;
278 14 50 33     249 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         37 my $sql = 'EXPLAIN QUERY PLAN ' . _bind($dbh, $ret, $params, $types);
284 14         61 $sth = $dbh->prepare($sql);
285 14         1034 $sth->execute;
286             } else {
287             # not supported
288 0         0 return;
289             }
290              
291 14 50       155 return if $DBI::err; # skip if maybe statement error
292             return sub {
293 14     14   39 my %args = @_;
294              
295 14 100 66     92 return $sth->fetchall_arrayref(+{}) unless defined $args{print} and $args{print};
296              
297 10         55 my $t = Text::ASCIITable->new();
298 10         254 $t->setCols(@{$sth->{NAME}});
  10         150  
299 10 50       951 $t->addRow(map { defined($_) ? $_ : 'NULL' } @$_) for @{$sth->fetchall_arrayref};
  10         238  
  40         180  
300              
301 10         1453 return $t;
302 14         145 };
303             }
304              
305             sub _bind {
306 182     182   385 my ($dbh, $ret, $params, $types) = @_;
307 182   100     659 $types ||= [];
308 182         273 my $i = 0;
309 182 50 33     2109 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 182         569 $ret =~ s/\?/$dbh->quote($params->[$i], $types->[$i++])/eg;
  16         230  
336             }
337 182         777 return $ret;
338             }
339              
340             sub _logging {
341 175     175   452 my ($class, $dbh, $ret, $time, $bind_params, $explain) = @_;
342              
343 175   100     826 my $threshold = $container->{threshold} || $ENV{DBIX_QUERYLOG_THRESHOLD};
344 175 100 66     464 return unless !$threshold || $time > $threshold;
345              
346 173   50     381 $bind_params ||= [];
347              
348 173         290 my $i = 0;
349 173         696 my $caller = { pkg => '???', line => '???', file => '???' };
350 173         1536 while (my @c = caller(++$i)) {
351 189 100 100     997 if (!$SKIP_PKG_MAP{$c[0]} and $c[0] !~ /^DB[DI]::/) {
352 173         724 $caller = { pkg => $c[0], file => $c[1], line => $c[2] };
353 173         465 last;
354             }
355             }
356              
357 173         347 my $sql = $ret;
358 173 100 100     722 if ($container->{skip_bind} || $ENV{DBIX_QUERYLOG_SKIP_BIND}) {
359 9         21 local $" = ', ';
360 9 50       20 if (@$bind_params) {
361 9 100       21 my @bind_data = map { defined $_ ? $_ : 'NULL' } @$bind_params;
  11         86  
362 9         41 $ret .= " : [@bind_data]";
363             }
364             }
365              
366 173 100 66     648 if ($container->{compact} || $ENV{DBIX_QUERYLOG_COMPACT}) {
367 2         7 my ($buff, $i) = ('', 0);
368 2         4 my $skip_space = 0;
369 2         4 my $before_escape = 0;
370 2         4 my $quote_char = '';
371 2         23 for (my ($i, $l) = (0, length $ret); $i < $l; ++$i) {
372 284         381 my $s = substr $ret, $i, 1;
373 284 100 100     1924 if (!$quote_char && ($s eq q{ }||$s eq "\n"||$s eq "\t"||$s eq "\r")) {
    100 100        
    100 100        
    100 66        
      100        
      100        
374 97 100       193 next if $skip_space;
375 46         84 $buff .= q{ };
376 46         53 $skip_space = 1;
377 46         85 next;
378             }
379             elsif ($s eq q{'} || $s eq q{"} || $s eq q{`}) {
380 20 100       56 unless ($quote_char) {
    100          
381 8         11 $quote_char = $s;
382             }
383 0 100       0 elsif (!$before_escape && $s eq $quote_char) {
384 7         10 $quote_char = '';
385             }
386             else {
387 5         6 $before_escape = 0;
388             }
389             }
390             elsif (!$before_escape && $quote_char && $s eq q{\\}) {
391 1         2 $before_escape = 1;
392             }
393             elsif (!$quote_char) {
394 133 100 100     457 if ($s eq q{(}) {
    100 66        
395 2         5 $buff .= $s;
396 2         2 $skip_space = 1;
397 2         5 next;
398             }
399             elsif (($s eq q{)}||$s eq q{,}) && substr($buff, -1, 1) eq q{ }) {
400 4         6 substr($buff, -1, 1) = '';
401             }
402             }
403 185         240 $buff .= $s;
404 185         320 $skip_space = 0;
405             }
406 2         51 ($ret = $buff) =~ s/^\s|\s$//g;
407             }
408              
409 173 100 66     651 if ($container->{useqq} || $ENV{DBIX_QUERYLOG_USEQQ}) {
410 2         6 local $Data::Dumper::Useqq = 1;
411 2         6 local $Data::Dumper::Terse = 1;
412 2         7 local $Data::Dumper::Indent = 0;
413 2         12 $ret = Data::Dumper::Dumper($ret);
414             }
415              
416 173   66     719 my $color = $container->{color} || $ENV{DBIX_QUERYLOG_COLOR};
417 173         340 my $localtime = do {
418 173         4214 my ($sec, $min, $hour, $day, $mon, $year) = localtime;
419 173         1337 sprintf '%d-%02d-%02dT%02d:%02d:%02d', $year + 1900, $mon + 1, $day, $hour, $min, $sec;
420             };
421 173         2447 my $data_source = "$dbh->{Driver}{Name}:$dbh->{Name}";
422             my $message = sprintf "[%s] [%s] [%s] %s%s at %s line %s\n",
423             $localtime, $caller->{pkg}, $time,
424             $container->{show_data_source} || $ENV{DBIX_QUERYLOG_SHOW_DATASOURCE} ? "[$data_source] " : '',
425             $color ? colored([$color], $ret) : $ret,
426 173 100 100     1783 $caller->{file}, $caller->{line};
    100          
427              
428 173 100       674 if (my $logger = $container->{logger}) {
429 3 100       41 my %explain = $explain ? (explain => $explain->()) : ();
430 3         242 $logger->log(
431             level => $LOG_LEVEL,
432             message => $message,
433             params => {
434             dbh => $dbh,
435             localtime => $localtime,
436             time => $time,
437             sql => $sql,
438             bind_params => $bind_params,
439             data_source => $data_source,
440             %explain,
441             %$caller,
442             },
443             );
444             }
445             else {
446 170 100       580 if (ref $OUTPUT eq 'CODE') {
447 11 100       41 my %explain = $explain ? (explain => $explain->()) : ();
448 11         225 $OUTPUT->(
449             dbh => $dbh,
450             level => $LOG_LEVEL,
451             message => $message,
452             localtime => $localtime,
453             time => $time,
454             sql => $sql,
455             bind_params => $bind_params,
456             data_source => $data_source,
457             %explain,
458             %$caller,
459             );
460             }
461             else {
462 159 100       251 print {$OUTPUT} $message, $explain ? $explain->(print => 1) : ();
  159         1045  
463             }
464             }
465             }
466              
467             {
468             package # hide from pause
469             DBIx::QueryLog::Guard;
470             sub new {
471 2     2   8 my ($class, $org_is_enabled) = @_;
472 2         9 bless [$org_is_enabled], shift;
473             }
474             sub DESTROY {
475 2 100   2   2596 if (shift->[0]) {
476 1         6 DBIx::QueryLog->enable();
477             }
478             else {
479 1         6 DBIx::QueryLog->disable();
480             }
481             }
482             }
483              
484             1;
485             __END__