File Coverage

lib/UR/DBI/Report.pm
Criterion Covered Total %
statement 15 201 7.4
branch 0 120 0.0
condition 0 30 0.0
subroutine 5 12 41.6
pod 0 3 0.0
total 20 366 5.4


line stmt bran cond sub pod time code
1              
2              
3             =pod
4              
5             =head1 NAME
6              
7             UR::DBI::Report - a database report interface
8              
9             =head1 SYNOPSIS
10              
11             ##- use UR::DBI::Report;
12             UR::DBI::Report->use_standard_cmdline_options();
13             UR::DBI::Report->generate(sql => \@ARGV);
14            
15              
16             =head1 DESCRIPTION
17              
18             This module is a reporting interface which takes SQL queries in a variety of forms
19             and prints their results with formatting options.
20              
21             =cut
22              
23              
24 266     266   967 use strict;
  266         345  
  266         7003  
25 266     266   866 use warnings;
  266         312  
  266         10150  
26              
27             package UR::DBI::Report;
28 266     266   903 use base 'UR::ModuleBase';
  266         281  
  266         106074  
29             require UR;
30             our $VERSION = "0.46"; # UR $VERSION;
31              
32 266     266   1414 use Data::Dumper;
  266         379  
  266         11198  
33 266     266   136694 use Time::HiRes;
  266         286789  
  266         872  
34              
35             # Support some options right on the "use" line.
36              
37             sub import
38             {
39 0     0     my $class = shift;
40 0           my %params = @_;
41 0 0         UR::DBI::Report->extend_command_line() if delete $params{extend_command_line};
42 0 0         die "Unknown options passed-to " . __PACKAGE__ . join(", ", keys %params) if keys %params;
43             }
44              
45              
46             # Applications which do no additional configuration will get these parameters by default.
47              
48             our %module_default_params =
49             (
50             delimit => 'spaces',
51             header => 1,
52             count => 1,
53             orient => 'vert',
54             trunc => 35,
55             sloppy => 0,
56             nulls => 1,
57             data => 1,
58             combine => 0,
59             "explain-sql" => 0,
60             );
61              
62             # Applications which call this method before init() will allow the user
63             # to override reporting defaults via standard command line options.
64              
65             our %application_default_params = %module_default_params;
66              
67             sub extend_command_line
68             {
69             # this callback processes all of the options and sets application defaults for this module
70 0     0 0   my $parse_option_callback;
71             $parse_option_callback =
72             sub
73             {
74 0     0     my ($flag,$value) = @_;
75 0 0         if ($flag eq 'parse')
76             {
77 0           $parse_option_callback->("header",!$value);
78 0           $parse_option_callback->("count",!$value);
79 0 0         $parse_option_callback->("delimit",($value ? "tabs" : "spaces"));
80 0 0         $parse_option_callback->("trunc",($value ? undef : $application_default_params{"trunc"}));
81 0           return 1;
82             }
83            
84 0           $application_default_params{$flag} = $value;
85 0           return 1;
86 0           };
87            
88             # ask Getopt to expect some new cmdline parameters
89             UR::Command::Param->add(
90             map {
91 0 0         if (ref($_)) {
  0            
92 0           $_->{module} = "Data Report Formatting"
93             }
94 0           $_;
95             }
96             delimit =>
97             {
98             action => $parse_option_callback,
99             msg => "spaces|tabs: spaces separate columns evenly, tab-delimited columns are easiliy parsed",
100             argument => '=s',
101             option => '--delimit',
102             },
103             header =>
104             {
105             action => $parse_option_callback,
106             msg => "Show column headers.",
107             argument => '!',
108             option => '--header',
109             },
110             data =>
111             {
112             action => $parse_option_callback,
113             msg => "Show returned query data (on by default!).",
114             argument => '!',
115             option => '--data',
116             },
117             count =>
118             {
119             action => $parse_option_callback,
120             msg => "Show row count at the end of output.",
121             argument => '!',
122             option => '--count',
123             },
124             orient =>
125             {
126             action => $parse_option_callback,
127             msg => "vert: (default) one row per output line, horiz: one row per output column.",
128             argument => '=s',
129             option => '--orient',
130             },
131             trunc =>
132             {
133             action => $parse_option_callback,
134             msg => "Set column truncation for long values. A zero setting truncates at the level of the default DBI LongReadLen; see DBI documentation for details.",
135             argument => '=s',
136             option => '--trunc',
137             },
138             sloppy =>
139             {
140             action => $parse_option_callback,
141             msg => "When processing multiple SQL statements and a failure occurs, just proceed to the next statement.",
142             argument => '!',
143             option => '--sloppy',
144             },
145             nulls =>
146             {
147             action => $parse_option_callback,
148             msg => "Show nulls. When turned-off with 'no-nulls' replaces them with a ?.",
149             argument => '!',
150             option => '--nulls',
151             },
152             parse =>
153             {
154             action => $parse_option_callback,
155             msg => "Equivalent to --noheader --nocount --tabs --trunc=0",
156             argument => '!',
157             option => '--parse',
158             },
159             echo =>
160             {
161             action => $parse_option_callback,
162             msg => "Print SQL before its first execution. Does not print multiple times on multiple executes with different params.",
163             argument => '!',
164             option => '--echo',
165             },
166             combine =>
167             {
168             action => $parse_option_callback,
169             msg => "When executing the same query multiple times with different params, combine the results as though it were one query.",
170             argument => '!',
171             option => '--combine',
172             },
173             "explain-sql" =>
174             {
175             action => $parse_option_callback,
176             msg => "Dump a query plan instead of running the query.",
177             argument => '!',
178             option => '--explain-sql',
179             }
180             );
181              
182             }
183              
184             #
185             # This method executes the specified sql statements and prints reports for each.
186             #
187              
188             sub generate
189             {
190 0     0 0   my $class = shift;
191 0           my %params = (%application_default_params, @_);
192              
193 0           my $sql_param = delete $params{sql};
194 0 0         my @queries = (ref($sql_param) ? (@$sql_param) : ($sql_param) );
195            
196 0           my $dbh = delete $params{dbh};
197 0 0         unless ($dbh) {
198 0           Carp::confess("No dbh sent to UR::DBI::Report, and no default available anymore!");
199             }
200              
201 0           $dbh->{LongTruncOk} = 1;
202 0 0         if ($params{trunc})
    0          
203             {
204 0           $dbh->{LongReadLen} = $params{trunc};
205             }
206             elsif(defined($params{trunc}))
207             {
208 0           warn "Setting the trunc value to 0 does not guarantee no truncating.";
209 0           warn "There is no way to completely prevent truncating in the current version of DBI.";
210 0           warn "The current trunc limit is $dbh->{LongReadLen}.";
211 0           warn "If this does not satisfy your needs, try setting trunc to a higher number";
212             }
213              
214              
215             # The outer loop runs once per SQL statement.
216 0           my $sql_request;
217 0           while($sql_request = shift(@queries))
218             {
219             # The SQL comes from the cmdline or STDIN.
220 0           my $sql;
221 0 0         if ($sql_request eq '-')
222             {
223 0           $sql = '';
224 0           while ()
225             {
226 0 0         next if (/^#!/);
227 0 0         if (/;\s*$/)
228             {
229 0           s/;\s*$//;
230 0           $sql .= $_;
231 0           last;
232             }
233             else
234             {
235 0           $sql .= $_;
236             }
237             }
238             }
239             else
240             {
241 0           $sql = $sql_request;
242             }
243            
244 0 0         next if ($sql !~ /\S/);
245            
246 0           chomp($sql);
247 0 0         print "SQLRUN: $sql\n" if $params{echo};
248              
249            
250             # See if we expect paramters from STDIN
251 0           my $question_marks = $sql;
252 0           $question_marks =~ s/[^\?]//msg;
253 0           my $question_mark_count = length($question_marks);
254              
255 0 0         if ($params{"explain-sql"}) {
256 0           my $outfh = IO::Handle->new;
257 0           $outfh->fdopen(fileno(STDOUT), 'w');
258 0           UR::DBI::_print_query_plan($sql,$dbh,outfh => $outfh,%params);
259            
260             # skip past any parameters, since we're not really executing,
261             # and they don't (can't) affect the query plan
262 0 0         if ($question_mark_count)
263             {
264 0           my $data;
265 0           while (1)
266             {
267 0           $data = ;
268 0           chomp $data;
269 0 0 0       last unless (defined($data) and length($data));
270             }
271             }
272            
273             # redo if we're reading from stdin, otherwise go to the next specified cmd
274 0 0         if ($sql_request eq '-') {
275 0           redo;
276             }
277             else {
278 0           next;
279             }
280             }
281            
282             # This will never get re-prepared
283 0           my $sth = $dbh->prepare_cached($sql);
284            
285 0 0         unless($sth)
286             {
287 0 0         if ($params{sloppy})
288             {
289 0           App::UI->error_message($dbh->errstr);
290 0           next;
291             }
292             else
293             {
294 0           die $dbh->errstr;
295             }
296             }
297              
298             # This flag may be set after the first parameter set runs to speed further executions.
299            
300             # The inner loop runs once per required execution of the SQL.
301             # SQL is executed multiple times if there are ? placeholders and there are multiple lines on STDIN
302 0           my ($combine_row_count, $combine_time)=(0, 0);
303 0           my $sql_execution_count = 0;
304 0           my $statement_is_not_a_query = 0;
305 0           my $outfh = $params{outfh};
306 0           for (1)
307             {
308             # Get params from STDIN if necessary
309 0           my @params;
310 0 0         if ($question_mark_count)
311             {
312             # Get data from STDIN as needed for any ?s.
313 0           my $data = ;
314 0 0         chomp $data if defined($data);
315              
316             # If we have a ? count and there is no data on this line, we're done with this SQL statement.
317 0 0 0       unless (defined($data) and length($data))
318             {
319             # We want to warn the user if a SQL statement had no params at all.
320 0 0         if ($sql_execution_count == 0)
321             {
322 0           $class->error_message("No params!");
323             }
324             # On to the next staement, if there is one.
325 0           last;
326             }
327 0           @params = split(/\t/, $data);
328 0           $#params = $question_mark_count - 1;
329            
330 0 0         if ($params{echo})
331             {
332 0           print "PARAMS: @params\n";
333             }
334             }
335            
336             # Note the time so we can show the elapsed time.
337 0           my $t1 = Time::HiRes::time();
338              
339             # Execute the current statement with the parameters.
340 0           my $execcnt;
341            
342 0 0         unless ($execcnt = $sth->execute(@params))
343             {
344 0 0         my $msg = "Failed to execute SQL:\n$sql\n" . (@params ? "Data:\n>" . join(",",@params) . "<\n" : '') . $sth->errstr;
345 0 0         if ($params{sloppy})
346             {
347 0           App::UI->error_message($msg);
348             }
349             else
350             {
351 0           die $msg;
352             }
353             }
354            
355             # Count these for better error messaging.
356 0           $sql_execution_count++;
357            
358             # Count results returned (SQL) or affected (DML).
359 0           my $rowcnt;
360            
361             # This flag may not be set until we try to get the first result.
362 0 0         unless ($statement_is_not_a_query)
363             {
364             $rowcnt = UR::DBI::Report->print_formatted(
365             sth => $sth,
366             outfh => $outfh,
367             (
368             $params{combine}
369 0 0         ? (position_in_combined_sql_list => $sql_execution_count)
370             :()
371             ),
372             %params
373             );
374 0 0 0       $statement_is_not_a_query = 1 if defined($rowcnt) and ($rowcnt eq "0 because the statement is not a query");
375             }
376            
377             # Flush any data pending to the output filter.
378 0 0 0       if (ref($outfh) and not $params{combine} and not $params{outfh}) {
      0        
379 0           $outfh->close;
380 0           $outfh = undef;
381             }
382            
383 0           $sth->finish;
384            
385             # Summarize the effect of the query/dml.
386 0 0         if ($params{count}) {
387 0 0         if($params{combine}) {
388             #If we're doing a combined output, we'll have to tally these up for later
389 0 0         $combine_row_count+=$statement_is_not_a_query?($execcnt+0):($rowcnt+0);
390 0           $combine_time+=Time::HiRes::time()-$t1;
391             } else {
392 0           my $td = Time::HiRes::time() - $t1;
393 0           $td =~ s/(\.\d\d\d).*/$1/;
394 0 0         if ($statement_is_not_a_query)
395             {
396 0           print (($execcnt+0) . " row(s) affected. Execution time: ${td} second(s).\n");
397             }
398             else
399             {
400 0           print (($rowcnt+0) . " row(s) returned. Execution time: ${td} second(s).\n");
401             }
402             }
403             }
404            
405             # By default this block will execute just once.
406             # Continue if there is a question_mark_count.
407             # It will "last" out at the top if there is no more data on stdin.
408 0 0         redo if $question_mark_count;
409            
410             } # end params loop
411              
412 0 0         if ($params{combine}) {
413 0 0 0       $outfh->close if ref($outfh) and not $params{outfh};
414 0 0         if ($params{count})
415             {
416 0           $combine_time=~s/(\.\d\d\d).*/$1/;
417 0 0         print("$combine_row_count row(s) ".($statement_is_not_a_query?'affected':'returned').
418             ". Execution time: $combine_time second(s).\n");
419              
420             }
421             }
422              
423             # If the cmdline sql was a dash, we're reading from STDIN until it exits the loop.
424 0 0         redo if $sql_request eq '-';
425            
426             } # end SQL loop
427            
428             # Done executing all SQL.
429 0           return 1;
430            
431             } # end of sqlrun subroutine
432              
433             # This method prints a single report for a given statement handle.
434              
435             sub print_formatted
436             {
437 0     0 0   my $class = shift;
438 0           my %params = (%application_default_params, @_);
439            
440             # sth A statement handle from which the data comes.
441             # sql If no handle is specified, the SQL to use.
442             # infh If no sth or sql is specified, a handle from which sql can be pulled.
443             # If sth or sql ARE specifed, a handle from which parameter values can be pulled.
444            
445 0           my $sth = delete $params{sth};
446 0 0         unless ($sth) {
447            
448             }
449            
450             # outfh An optional handle to which the report is written.
451            
452 0           my $outfh = delete $params{outfh};
453 0 0         if ($outfh) {
454 0 0 0       if ($params{delimit} =~ /^s/i && $^O ne "MSWin32" && $^O ne 'cygwin')
      0        
455             {
456             # We only handle one case of $outfh and still do tab2col.
457             # If it's stderr, we redirect there.
458 0           $outfh = IO::File->new('| tab2col --nocount 1>&' . fileno($outfh));
459 0 0         Carp::confess("Failed to pipe through tab2col!") unless $outfh;
460             }
461             }
462             else {
463 0 0         if ($params{delimit} =~ /^s/i)
464             {
465             # Handle tab-delimit via tab2col
466 0           $outfh = IO::File->new("| tab2col --nocount");
467             }
468             else
469             {
470 0           $outfh = IO::Handle->new;
471 0           $outfh->fdopen(fileno(STDOUT), 'w');
472             }
473             }
474            
475             # This is the return value.
476             # Set to an integer, or to the false-valued string "0 because the statement is not a query".
477 0           my $rowcnt = 0;
478            
479             # Get the column names into an array of headers.
480 0           my @headers = @{ $sth->{NAME_uc} };
  0            
481            
482            
483             # Display as needed according the requested orientation.
484 0 0         if ($params{orient} =~ /^v/i) # lines listed vertically
    0          
485             {
486             # Get the first row, but re-hook warnings first to see if we
487             # are really running a query wich can return data (not DML).
488 0           my $msg;
489 0     0     local $SIG{__WARN__} = sub { $msg = shift };
  0            
490            
491 0           my $row = $sth->fetchrow_arrayref;
492              
493 0 0         if ($msg =~ /ERROR no statement executing/)
    0          
494             {
495             # Set this flag so we do not re-try fetch*() on this query.
496 0           return "0 because the statement is not a query";
497             }
498             elsif ($sth->errstr) {
499 0           die $sth->errstr;
500             }
501             else
502             {
503 0 0         if ($params{data})
504             {
505             # Spacers are dashes.
506 0           my @spacers = @headers;
507 0           for (@spacers) { $_ =~ s/./-/g }
  0            
508            
509             # Print the headers, a line of spacers, then one line for each result row.
510 0 0 0       if ($params{header} and not ($params{combine} and $params{position_in_combined_sql_list} > 1))
      0        
511             {
512 0 0         if (my $trunc = $params{trunc})
513             {
514 0           for my $row (\@headers, \@spacers)
515             {
516 0           print $outfh join("\t", map { substr($_,0,$trunc) } @$row) . "\n";
  0            
517             }
518             }
519             else
520             {
521 0           for my $row (\@headers, \@spacers)
522             {
523 0           print $outfh join("\t",@$row) . "\n";
524             }
525             }
526             }
527            
528             # Print the initial row, and any others we can fetch().
529 0           while ($row)
530             {
531 0           print $outfh join("\t",@$row) . "\n";
532 0           $rowcnt++;
533 0           $row = $sth->fetchrow_arrayref;
534             }
535            
536             }
537             else
538             {
539             # Just get the count
540 0           while ($row) { $rowcnt++; $row = $sth->fetchrow_arrayref }
  0            
  0            
541             }
542             }
543             }
544             elsif ($params{orient} =~ /^h/i)
545             {
546 0           my $msg;
547 0     0     local $SIG{__WARN__} = sub { $msg = shift };
  0            
548            
549 0           my $results = $sth->fetchall_arrayref;
550            
551 0 0         if ($msg =~ /ERROR no statement executing/)
552             {
553             # Set this flag so we do not re-try fetch*() on this query.
554 0           return "0 because the statement is not a query";
555             }
556             else
557             {
558             # Process the fetched data.
559 0           $rowcnt = scalar(@$results);
560            
561 0 0         if ($params{data})
562             {
563             # Show the data
564 0           my $cnum = 0;
565            
566 0 0         if (my $trunc = $params{trunc})
567             {
568 0           for my $header (@headers)
569             {
570 0 0         print $outfh $header . "\t:\t" if ($params{header});
571 0           print $outfh join("\t", map { substr($_->[$cnum],0,$trunc) } @$results) . "\n";
  0            
572 0           $cnum++;
573             }
574             }
575             else
576             {
577 0           for my $header (@headers)
578             {
579 0 0         $outfh->print($header . "\t:\t") if ($params{header});
580 0           $outfh->print(join("\t", map { $_->[$cnum] } @$results) . "\n");
  0            
581 0           $cnum++;
582             }
583             }
584             }
585             }
586             }
587             else
588             {
589 0           $class->error_message("Unknown orientation $params{orient}");
590 0           return;
591             }
592              
593 0           return $rowcnt;
594             }
595              
596             1;
597