File Coverage

blib/lib/DBIx/Repgen.pm
Criterion Covered Total %
statement 105 138 76.0
branch 32 72 44.4
condition 12 36 33.3
subroutine 12 15 80.0
pod 7 11 63.6
total 168 272 61.7


line stmt bran cond sub pod time code
1             package DBIx::Repgen;
2              
3 1     1   11680 use 5.006;
  1         5  
  1         53  
4 1     1   7 use strict;
  1         3  
  1         44  
5 1     1   7 use Carp;
  1         17  
  1         518  
6             our $VERSION = '0.01';
7              
8              
9             ##############################################################################################
10             ##############################################################################################
11             ##############################################################################################
12              
13             =head1 NAME
14              
15             DBIx::Repgen - simple report generator from DB-selected data
16              
17             =head1 SYNOPSIS
18              
19             use Repgen;
20              
21             $r = DBIx::Repgen->new(
22             dbh => DBI->connect(...),
23             query => 'select ... from ...',
24              
25             repdata => {
26             today => `date`
27             },
28              
29             group => ['id'],
30             header => "========\n",
31             footer => sub {my ($r, $data) = @_; return "$data->{NAME} : $data->{VALUE}"},
32             item => ["%20s %s", qw/NAME VALUE/],
33              
34             output => \$out;
35             );
36              
37             $r->run(cust => 'tolik');
38             print $out;
39              
40             =head1 DESCRIPTION
41              
42             This package implements class C, which is simple report generator from data
43             received from relational database by some select-statement. Such a report can contain
44             hyerarchical grouping by field values, record counters and cumulative totals (sums) of numeric
45             fields for each group as well as for whole report. Each rerort part formatting may be set
46             as literal string, arguments of C function or be code reference.
47              
48             =head2 new, class constructor
49              
50             Constructor has one argument, hashe. Elements of this hashe define the report and are
51             descriebed below.
52              
53             =over
54              
55             =item sth, dbh, query - data source setting
56              
57             The report data are got by executing some select statement against relational database
58             environment. There are following wais for defining this statement.
59              
60             =over
61              
62             =item 1.
63              
64             Constructor receives in C element prepared (C<$dbh->prepare>) but not executed
65             (C<$sth->execute>) statement handle.
66              
67             =item 2.
68              
69             Constructor receives database connection handle (from Cconnect(...)>) and full text
70             of select statement to be executed. Needed C and C calls will perform
71             by the report run.
72              
73             =item 3.
74              
75             Constructor receives already prepared and executed statement handle. In this case C
76             constructor parameter must be set to true. This feature may be useful by dynamic-made select
77             queryes in calling programm. No prepare nor execute action will be performed by report run.
78              
79             I: you have to reset (by C method) this statemeny handle before each next
80             report run.
81              
82             =back
83              
84             Samples:
85              
86             $dbh = DBI->connect('dbi:Oracle:SID', 'user', 'password');
87             $sth1 = $dbh->prepare('select name, value from tab where value between ? and ?');
88             $rep1 = DBIx::Repgen->new(sth => $sth);
89              
90             $rep2 = DBIx::Repgen->new(dbh => $dbh, query => "select ... ");
91              
92             $sth3 = $dbh->prepare('select ...');
93             $sth3->execute(@param);
94             $rep3 = DBIx::Repgen->new(sth => $sth3, noexec => 1);
95              
96             Using first two methods you may parametrize the report. This means sql-query can contain
97             placeholders, for substituting values in report run time. See below about report parameters.
98              
99             =item param - report parameters
100              
101             The report may have set of named parameters. Single parameter definition contain its name,
102             number (or some numbers) of placeholders in source select query and optional default value.
103             Parametrs definition is a hash reference, value of C element of constructor. Keys in this
104             hash are parameter names and values contain placeholder numbers and default values.
105              
106             In the simpliest case parameter definition can be just zero-based number of the only placeholder
107             corresponding to this parameter. In more complex cases is is hash reference. This hash I
108             have C key with value of integer or list of integers and I have C key, which
109             value must be scalar, code reference or array reference (where first element is code reference).
110              
111             The C key defines zero based number (or numbers) of placeholdes in source select query
112             corresponding to this parameter. The C key defines default value for optional
113             parameters. If value of C is code reference then default value is result of this code call (without
114             arguments). If value of C is array reference then first element of this array must
115             be code reference. Default value of parameter in this case is result of call this code with arguments -
116             the rest of array.
117              
118             Sample of parameter definition.
119              
120             $rep = DBIx::Repgen->new(
121             ...
122             param => {
123             name => 0,
124             dep => {n => 1},
125             startdate => {n => [2, 4], dflt => '2000/01/01'},
126             enddate => {n => 3, dflt => \&DefEndDate},
127             salary => {n => 5, dflt => [sub {...}, 1000, 2000]}
128             }
129             );
130              
131             In the example C and C are required parameters corresponding to zero and first placeholders.
132             C has explicit default value and substituted to second and fouth placeholders.
133             C and C have defaults defining by code call in report run time, without and
134             with arguments in correspondence.
135              
136              
137             =item output - the way of report output
138              
139             The C constructor's parameter sets how and where the report puts its output data.
140              
141             =over
142              
143             =item undef or not present
144              
145             The whole output data are the result of C method call.
146              
147             =item string reference
148              
149             The output data are put into this string.
150              
151             =item code reference
152              
153             This code will be called with two arguments: the report object and string to be out.
154              
155             =back
156              
157             Output samples.
158              
159             $r = DBIx::Repgen(...);
160             print $r->run();
161              
162             $s = '';
163             $r = DBIx::Repgen(..., output => \$s,);
164             $r->run();
165             print $s;
166              
167             sub myprint {
168             my ($r, $s) = @_;
169             print "*** $s ***";
170             }
171             $r = DBIx::Repgen(..., output => \&myprint,);
172             $r->run();
173              
174             =item group - repport groupping
175              
176             The report may be I. The group is sequence of records having the constant value of some
177             field. This field called I. The report may have several includded groups. For
178             group setting you have to define C parameter of report constructor as a reference to
179             an array of group fields.
180              
181             Note that the right record's sequence must be provided by C part in used select query, not
182             by report itself. Sample of grouping by countries and cities.
183              
184             $r = DBIx::Repgen->new(
185             ...,
186             query => "select country, city, population from cities
187             order by country, city",
188             group => [qw/COUNTRY CITY/],
189             ...
190             );
191              
192             Note I field names are in uppercase, regardless used database server.
193              
194             =item total - cumulative totals
195              
196             Value of this argument of constructor is reference to array with report fields to compute
197             totals. Each field summation executed for all the report as well as for each group. See
198             below about access to totals values.
199              
200             =item header, footer, item etc. - definition of report parts
201              
202             There are following I generated during report output.
203              
204             =over
205              
206             =item item
207              
208             Outputs for each record of the report.
209              
210             =item header
211              
212             Begin of whole report.
213              
214             =item footer
215              
216             Outputs after all, in the very end of report.
217              
218             =item header_GROUPFIELD
219              
220             Outputs in the begin of record group by GROUPFIELD field.
221              
222             =item footer_GROUPFIELD
223              
224             Outputs after record group by GROUPFIELD field.
225              
226             =back
227              
228             Each of these report pats may be defined by several ways.
229              
230             =over
231              
232             =item string
233              
234             The string will be printed "as is", without any processing.
235              
236             $r = DBIx::Repgen->new(
237             header => "\t\tReport about countries and cities\n",
238             ...
239             );
240              
241              
242             =item reference to array of strings
243              
244             First element of this array have to be in form of C function format. The rest
245             of values in the array are I (not values!) of current report data. See below
246             about current report data.
247              
248             $r = DBIx::Repgen->new(
249             footer => ["Total %d countries, %d cities, population %d people\n",
250             qw/num_COUNTRY num_CITY sum_POPULATION/],
251             ...
252             );
253              
254             =item code reference
255              
256             The code is called with two arguments: report object and hash reference storing
257             current report data. Subroutine may use C method for output any needed
258             information or just return output string as its result.
259              
260             $r = DBIx::Repgen->new(
261             item => sub {
262             my ($r, $d) = @_;
263             $r->Output("%d %s",
264             $d->{POPULATION},
265             $d->{POPULATION} > 1_000_000 ? '*' : ' ');
266             }
267              
268             footer => sub {return "Report ended at " . `date`}
269             ...
270             );
271              
272             =item reference to array where first element is code reference
273              
274             The code is called with following arguments: report object, current report data, the rest of
275             array elements.
276              
277             $r = DBIx::Repgen->new(
278             header_COUNTRY => [\&hfcountry, 'header'],
279             header_COUNTRY => [\&hfcountry, 'footer'],
280             ...
281             );
282              
283             sub hfcountry {
284             my ($r, $d, $header_or_footer) = @_;
285             if ($header_or_footer eq 'header') {...} else {...};
286             }
287              
288             =item max_items - max record number limit
289              
290             If this parameter (integer number) is present then no more than C records will be
291             output. It is possible to know via C method call if not all records were output.
292              
293             =back
294              
295             =head3 Current report data
296              
297             All report state data are stored in internal report variables. Access to these data from
298             report parts is possible by data names. There are following fields in current
299             report data.
300              
301             =over
302              
303             =item FIELDNAME
304              
305             Fields of current report's record. Name is in I.
306              
307             =item prev_FIELDNAME
308              
309             Value of FIELDNAME in previous record. When group boundary is detected group field has new value,
310             but its previous value is still stored. This value can be used in group footers.
311              
312             =item num_report
313              
314             Number (one-based) of current output record for the whole report. This counter never resets.
315              
316             =item num_item
317              
318             Number of record in the innermost group.
319              
320             =item num_GROUPNAME
321              
322             Number of group GROUPNAME in including group.
323              
324             =item total_FIELDNAME
325              
326             Cumulative total of FIELDNAME field for the whole report. Remember FIELDNAME must be listed
327             in C constructor's parameter.
328              
329             =item total_GROUPNAME_FIELDNAME
330              
331             Cumulative total by FIELDNAME field into GROUPNAME. These summators are reset each time
332             the group boundary is reached.
333              
334             =back
335              
336              
337             =back
338              
339              
340             =cut
341              
342              
343 1     1   8 use strict;
  1         1  
  1         3292  
344              
345             sub new {
346 1     1 1 3425 my ($class, %par) = @_;
347              
348 1   33     7 return bless \%par, ($class || ref $class);
349             }
350              
351             =head2 run, report execution
352              
353             $r->run(%param);
354              
355             The report is run and output. Input parameters are substituted as values for select query
356             placeholders (see above about report's parameters). If there was no C constructor's parameter
357             then the text of report returned as a result of this method.
358              
359             =cut
360              
361             sub run {
362 1     1 1 6 my ($rep, %param) = @_;
363              
364 1         4 my $warn = $^W;
365 1         3 $^W = 0;
366              
367 1 50       15 unless ($rep->{sth}) {
368 0 0       0 croak "Missing 'dbh' arg" unless exists $rep->{dbh};
369 0 0 0     0 croak "Missing or non-select query" unless $rep->{query} && $rep->{query} =~ /^\s*select\b/si;
370 0         0 $rep->{sth} = $rep->{dbh}->prepare($rep->{query});
371             }
372              
373 1 50       5 unless ($rep->{output}) {
374 1         4 $rep->{outputstr} = '';
375 1         3 $rep->{output} = \$rep->{outputstr};
376             }
377              
378 1         2 delete $rep->{not_first};
379              
380 1         5 $rep->{data} = {num_report => 0, num_item => 0};
381              
382 1 50       6 $rep->{param} = {} unless exists $rep->{param};
383 1         3 my @param = ();
384 1 50       4 goto AFTEREXEC if $rep->{noexec};
385 1         4 for my $p (keys %{$rep->{param}}) {
  1         9  
386 0 0       0 $rep->{param}{$p} = {n => $rep->{param}{$p}}
387             unless ref($rep->{param}{$p});
388 0 0       0 croak "No positions are given for '$p' parameter" unless exists $rep->{param}{$p}{n};
389              
390 0         0 my @n;
391 0 0       0 if (ref ($rep->{param}{$p}{n}) eq 'ARRAY') {
    0          
392 0         0 @n = @{$rep->{param}{$p}{n}};
  0         0  
393             } elsif (!ref($rep->{param}{$p}{n})) {
394 0         0 @n = ($rep->{param}{$p}{n});
395             } else {
396 0         0 croak "Non scalar nor array reference positions for '$p' parameter";
397             }
398              
399 0         0 my $val;
400 0 0 0     0 if (defined($param{$p}) && $param{$p} ne '') {
    0          
401 0         0 $val = $param{$p};
402             } elsif (defined $rep->{param}{$p}{dflt}) {
403 0         0 $val = $rep->{param}{$p}{dflt};
404 0 0 0     0 unless (ref $val) {
    0 0        
    0          
405             # nothing
406             } elsif (ref($val) eq 'CODE') {
407 0         0 $val = $val->();
408             } elsif (ref($val) eq 'ARRAY' && $val->[0] && (ref($val->[0]) eq 'CODE')) {
409 0         0 my ($sub, @pars) = @$val;
410 0         0 $val = $sub->(@pars);
411             } else {
412 0         0 croak "Wrong dflt for '$p' parameter";
413             }
414             } else {
415 0         0 croak "Cannot determine value for parameter '$p'";
416             }
417 0         0 $param[$_] = $val for grep {$_ >= 0} @n;
  0         0  
418 0         0 $rep->{data}{"param_$p"} = $val;
419             }
420 1         10 $rep->{sth}->execute(@param);
421 1         20 AFTEREXEC:
422              
423              
424             # Заголовок отчета
425             $rep->PrintPart('header');
426              
427             # строки отчета
428 1         23 while ($rep->{row} = $rep->{sth}->fetchrow_hashref('NAME_uc')) {
429 9         193 $rep->PrintItem();
430 9 50 33     25 $rep->Abort() if $rep->{max_items} && $rep->{max_items} <= $rep->{data}{num_report};
431 9 50       17 last if $rep->Aborted();
432             }
433 1         24 $rep->{sth}->finish();
434              
435             # Если надо - завершители групп после отчета
436 1 50       4 if (exists $rep->{group}) {
437             # Формируем "пустую" строку
438 1         3 for (keys %{$rep->{data}}) {
  1         5  
439 11 100       34 $rep->{row}{$1} = undef if /prev_(.+)/;
440             }
441 1         4 $rep->PrintHeaderFooter(0, 'footer');
442             }
443              
444             # Завершитель отчета
445 1         13 $rep->PrintPart('footer');
446              
447 1         2 $^W = $warn;
448              
449             # Закрыть коннекцию если надо
450 1 50 33     6 $rep->{dbh}->disconnect() if $rep->{dbh} && $rep->{autoclose};
451              
452 1         11 return $rep->{outputstr};
453             }
454              
455             sub PrintItem {
456 9     9 0 10 my ($r) = @_;
457              
458             # Скопировать поля строки в data
459 9         10 $r->{data}{$_} = $r->{row}{$_} for keys %{$r->{row}};
  9         59  
460              
461             # Продвинуть "сквозные" сумматоры по полям
462 9 50       25 if (exists $r->{total}) {
463 9         9 $r->{data}{'total_' . $_} += $r->{row}{$_} for @{$r->{total}};
  9         41  
464             }
465              
466             # Есть ли граница группы?
467 9         21 my $group = $r->GroupGranze();
468              
469             # Если это не самый первый раз - вывести завершители групп
470 9 100 100     41 $r->PrintHeaderFooter($group, 'footer')
471             if defined $group && $r->{not_first};
472              
473             # Установить, что уже - не первый раз
474 9         13 $r->{not_first} = 1;
475              
476             # Продвинуть сквозной номер и номер в пределах младшей группы
477 9         13 $r->{data}{num_report} ++;
478 9         10 $r->{data}{num_item} ++;
479              
480             # Вывести заголовок группы (при этом сбрасываются сумматоры и нумераторы)
481 9 100       24 $r->PrintHeaderFooter($group, 'header') if defined $group;
482              
483             # Просуммировть групповые сумматоры
484 9 50 33     43 if ($r->{group} && $r->{total}) {
485 9         9 for my $grname (@{$r->{group}}) {
  9         17  
486 9         9 $r->{data}{'total_' . $grname . '_' . $_} += $r->{row}{$_} for @{$r->{total}};
  9         47  
487             }
488             }
489              
490             # Вывести итем
491 9         23 $r->PrintPart('item');
492              
493             # Записать в $data предыдущие значения строки
494 9         10 $r->{data}{'prev_' . $_} = $r->{row}{$_} for keys %{$r->{row}};
  9         65  
495              
496 9         17 1;
497             }
498              
499             sub PrintHeaderFooter {
500 6     6 0 10 my ($r, $group, $hf) = @_;
501 6         6 my @group = @{$r->{group}};
  6         34  
502              
503             # Если заголовок
504 6 100       15 if ($hf eq 'header') {
505             # Сбросить сумматоры для каждой группы старше указанной
506 3 50       9 if ($r->{total}) {
507 3         7 for my $grname ((@group)[$group .. $#group]) {
508 3         2 $r->{data}{'total_' . $grname . '_' . $_} = 0 for @{$r->{total}};
  3         19  
509             }
510             }
511              
512             # И нумераторы
513 3         10 $r->{data}{'num_' . $r->{group}[$group]}++;
514 3         9 $r->{data}{'num_' . $_} = 1 for (@group)[$group+1 .. $#group];
515 3         6 $r->{data}{'num_item'} = 1;
516             }
517              
518             # Таки напечатать заголовки или завершители
519 6         24 $r->PrintPart($hf . '_' . $_) for (@group)[$group .. $#group];
520             }
521              
522             sub GroupGranze {
523 9     9 0 10 my ($r) = @_;
524              
525 9 50       21 return undef unless $r->{group};
526              
527 9         10 my $i = 0;
528 9         15 for my $fname (@{$r->{group}}) {
  9         17  
529 9 50       22 croak "No '$fname' group field in data" unless exists $r->{row}{$fname};
530              
531 9 100 66     82 return $i if
      66        
532             !exists($r->{data}{'prev_' . $fname}) ||
533             (
534             (($r->{data}{'prev_' . $fname} ne $r->{row}{$fname})) ||
535             (($r->{data}{'prev_' . $fname} != $r->{row}{$fname}))
536             );
537 6         13 $i++;
538             }
539              
540 6         12 undef;
541             }
542              
543              
544             sub PrintPart {
545 17     17 0 79 my ($r, $part) = @_;
546              
547 17 50       36 return unless $r->{$part};
548              
549 17         17 my ($fmt, @par);
550 17 100 33     46 if (ref($r->{$part}) eq 'ARRAY') {
    50          
551 14         16 ($fmt, @par) = @{$r->{$part}};
  14         43  
552             } elsif (ref($r->{$part}) eq 'CODE' || !ref($r->{$part})) {
553 3         5 ($fmt, @par) = ($r->{$part});
554             } else {
555 0         0 croak sprintf("Non supported type of format: '%s'", ref($r->{$part}));
556             }
557              
558 17         24 my $s;
559 17 100       28 if (ref $fmt) {
560 4         16 $s = $fmt->($r, $r->{data}, @par);
561             } else {
562 13         18 $s = sprintf($fmt, map {$r->{data}{$_}} @par);
  30         92  
563             }
564              
565 17         73 $r->Output($s);
566             }
567              
568             =head2 Output
569              
570             $r->Output("Any values: %s and %d", 'qazwsx', 654);
571              
572             This method has the same arguments as C function. It adds formatted string to the output
573             stream (set by C param). This method is useful in the code called during the output of
574             report parts.
575              
576             =cut
577              
578             sub Output {
579 17     17 1 24 my ($r, $s, @par) = @_;
580              
581 17 50       35 $s = sprintf($s, @par) if @par;
582              
583 17 50       56 if (ref($r->{output}) eq 'CODE') {
    50          
584 0         0 $r->{output}->($r, $s);
585             } elsif (ref($r->{output}) eq 'SCALAR') {
586 17         49 ${$r->{output}} .= $s;
  17         68  
587             } else {
588 0         0 croak "Non supported output method";
589             }
590             }
591              
592             =head2 Get, querying of report parameters
593              
594             @group = @{$r->Get('group')};
595              
596             Method returns value of named parameter which is set in constructor or via C method.
597              
598             =cut
599              
600             sub Get {
601 0     0 1 0 my ($r, $name) = @_;
602 0         0 return $r->{$name};
603             }
604              
605             =head2 Set, setting report parameters
606              
607             $r->Set(
608             header => "Very new header",
609             item => ["%s %s", qw/NAME VALUE/]
610             );
611              
612             Method redefines report parameters.
613              
614             =cut
615              
616             sub Set {
617 0     0 1 0 my ($r, %set) = @_;
618 0         0 while (my ($k, $v) = each %set) {
619 0         0 $r->{$k} = $v;
620             }
621             }
622              
623             =head2 Abort
624              
625             $r->Abort();
626              
627             Being called in the code it breaks report running.
628              
629             =cut
630              
631 0     0 1 0 sub Abort {$_[0]->{aborted} = 1}
632              
633             =head2 Aborted
634              
635             if ($r->Aborted()) {...}
636              
637             Method returns true if report execution was aborted by C method.
638              
639             =cut
640              
641 9     9 1 114 sub Aborted {$_[0]->{aborted}}
642              
643             1;
644              
645             __END__