/[gedafe]/trunk/lib/perl/DBIx/PearlReports.pm
This is repository of my old source code which isn't updated any more. Go to git.rot13.org for current projects!
ViewVC logotype

Contents of /trunk/lib/perl/DBIx/PearlReports.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1 - (show annotations)
Mon Feb 14 18:52:26 2005 UTC (19 years, 2 months ago) by dpavlin
File size: 15111 byte(s)
import of Gedafe 1.2.2

1 =pod
2
3 =head1 NAME
4
5 DBIx::PearlReports -- A Versatile Report Generator
6
7 =head1 SYNOPSIS
8
9 Using the SIMPLE mode
10
11 use DBIx::PearlReports qw(:Simple);
12 create (
13 -datasource => 'dbi:Pg:dbname=database;host=hostname',
14 -query => 'SELECT * FROM customers ORDER BY state, city;'
15 );
16
17 or
18
19 create (
20 -datasource => 'dbi:Pg:dbname=database;host=hostname',
21 -query => 'SELECT * FROM customers ORDER BY state, city WHERE name = ? AND age = ?',
22 -param => ['Tobi',34],
23 );
24
25 group (
26 -trigger => sub { $filed{state} },
27 -head => sub { "State: $field{state}\n" },
28 -foot => sub { "Average Age for $field{state}".rpavg($field{age}) }
29 );
30
31 group (
32 -trigger => sub { $field{city} },
33 -head => sub { "City: $field{city} (".rpcnt($field{name}.")\n" },
34 -foot => sub { "Total Customers in Customers in ".
35 "$field{city}: ".rpcnt($field{name})."\n" }
36 );
37
38 body (
39 -contents => sub { "$field{firstname} $field{lastname} $field{age}\n" }
40 );
41
42 print makereport;
43
44 PearlReports can also be used in an Object Oriented Context:
45
46 #!/usr/sepp/bin/perl-5.8.0
47 use lib qw( /usr/pack/postgresql-7.3.2-ds/lib/site_perl /usr/isgtc/lib/perl);
48 use DBIx::PearlReports;
49 $r = DBIx::PearlReports::create ( ... );
50 $r->group( ... );
51 $r->body( ... )
52 print $r->makereport;
53
54
55 =head1 DESCRIPTION
56
57 B<PearlReports> is a system for pulling information from an SQL database and
58 produce Reports from this information. B<PearlReports> provides a very
59 flexible system for creating reports based on SQL queries
60
61 While it is sufficient to use the simple statements provided by
62 PearlReports to create your reports, the full power of perl is only a
63 keystroke away.
64
65 Creating a Report using the PearlReports involves writing a short perl
66 script which first loads the PearlReports module:
67
68 #!/usr/bin/perl
69 use DBIx::PearlReports qw(:SIMPLE);
70
71 Then you call the B<create> function to create a new report:
72
73 create (
74 -username => 'myname',
75 -datasource => 'dbi:Pg:dbname=customers',
76 -query => 'SELECT * from customers ORDER by state,city'
77 );
78
79 When creating a report you have to define username and password for
80 the database you want to access. Because different people may use the
81 report. If you do not mention either the B<-username> or B<-password>
82 arguments, the module will ask you to supply one at run time.
83
84 The B<-datasource> argument defines which
85 database this report is going to use. Check the DBI/DBD documentation
86 for the syntax apropriate for your Database. In the example above we
87 are accessing a PostgreSQL database called I<customers> which runs on
88 the local host. The B<-query> argument of the create function defines
89 the data we want to use in our report.
90
91 A central element of PearlReports is the ability to work with groups of
92 records. In this example the report contains two nested groups. Note that
93 the data
94
95 must arive from the database in an order which is comatible with the
96 required groups. (Check the ORDER BY cause above).
97
98 group
99 ( -trigger => sub { $field{state} },
100 -head => sub { "Customers from $field{state}\n" },
101 -foot => sub { "Avg Age for $field{state} Customer:".
102 rpavg($field{age})."\n\n" } );
103
104 group
105 ( -trigger => sub { $field{zip} },
106 -head => sub { "Customers from $field{town}\n" },
107 -foot => sub { "Min Age in $field{town}:".
108 rpmin($field{age})."\n" } );
109
110
111 Each group definition requires a trigger and either a header or a
112 footer or both. Each argument of the group definition is a little perl
113 function definition. The Trigger function gets called for each record
114 in the query. Whenever the value returned from the
115 function changes the group iterates. Each iteration of a group is
116 enclosed by the apropriate header and footer.
117
118 The B<$field{xxx}> variables refer to the columns of the query.
119 The footer and the header (!) can contain agregation functions
120 (rpmin, rpmax, rpbig, rpsmall, rpsum, rpavg). See the section below
121
122 Finally the actual data can get printed.
123
124 body
125 ( -contents => sub { "$field{firstname} $field{lastname} $field{age}\n" } );
126
127 The body function will get called for each row in the database and its
128 return value will be printed into the report.
129
130 When groups and body are set up you can create the actual report by
131 executing:
132
133 print makereport;
134
135 When you want to make a new report using the existion database connection
136 for another report, you can reset it with the command
137
138 reset;
139
140 =head2 METHODS
141
142 All functions provided by PearlReports expect named arguments.
143
144 =head3 create or new
145
146 Defines the data the report should be based on. This involves
147 configuring the parameters for accessing the database server as well
148 as defining the query string.
149
150 If you are working with the OO interface you can use b<new> instead
151 of create to be more in line with established naming conventions.
152
153 =over
154
155 =item -username
156
157 the username for the database. If this option is not set, PearlReports will
158 prompt for a username.
159
160 =item -password
161
162 the password. If this option is not supplied PearlReports will prompt for a
163 password.
164
165 =item -datasource
166
167 the DBI connect string. Check the DBI/DBD manpage for the syntax apropriate
168 for your database.
169
170 =item -handle
171
172 instead of the previous 3 arguments you can also give PearlReports an
173 existing database handle to use.
174
175 =item -query
176
177 A SELECT query
178
179 =item -param
180
181 If you use '?' placeholders in the query, you can supply contents for them
182 using the reference to an arry holding the relevant data.
183
184 =back
185
186
187 =head2 group
188
189 The 'salt' of most reports is that some grouping structure exists. The
190 records in the report get collected into groups of records which bear
191 some common feature. Each group can have a header and a footer.
192
193 =over
194
195 =item -trigger
196
197 Is a pointer to a anonymous function. The function gets called for
198 each row in the result of the query. Whenever the return value from
199 this function changes the group goes into its next iteration. There
200 are other events which can cause a new iteration: A higher order group
201 goes into another iteration or the last record from the query has been
202 consumed.
203
204 =item -head and -foot
205
206 After each iteration, the anonymouse functions pointed to by the head
207 and foot options get executed. The return values from the functions
208 are used as header and footer for the material inside the group.
209
210 =back
211
212 =head3 body
213
214 The inner most 'group' of the report does have neither foot nor head,
215 it has just a body which gets printed for every row in the query result.
216
217 =over
218
219 =item -contents
220
221 Stores a pointer to an anonymous function which gets executed for each
222 record returned by the query.
223
224 =back
225
226 =head3 makereport
227
228 returns an array containing the report ...
229
230 =head3 reset
231
232 clears the group and body data from the report. This can be used to run a
233 second report of the same database connection without reconnecting.
234
235 =head2 AGGREGATE FUNCTIONS
236
237 =head3 rpsum
238
239 Builds the sum of all the values its argument takes during the
240 traversal of the records in the current group iteration.
241
242 =head3 rpmin, rpmax
243
244 Finds the min and max values in a group iteration.
245
246 =head3 rpsmall, rpbig
247
248 Finds the first and last value when sorting alphabetically.
249
250 =head3 rpcnt
251
252 Count the rows in the current group iteration.
253
254 =head3 rpavg
255
256 The same as above only that the average gets calculated.
257
258 =head2 HOW TO WRITE NEW AGGREGATE FUNCTIONS
259
260 If you want to write your own aggregat functions. Follow the examples
261 below. Note that the first two lines of each function are
262 mandatory. The structure you store in the $arr is up to you.
263
264 use PearlReports qw(:MyAgg);
265
266 sub mycnt ($) {
267 my $cnt = $aggmem->{counter}++;
268 my $arr = \$aggmem->{array}->[$cnt];
269 $$arr++
270 return $$arr;
271 }
272
273 sub myavg ($) {
274 my $cnt = $aggmem->{counter}++;
275 my $arr = \$aggmem->{array}->[$cnt];
276 $$arr->{sum} += $_[0];
277 $$arr->{cnt}++;
278 return $$arr->{sum} / $$arr->{cnt};
279 }
280
281 If you create cool aggregate functions please drop me a line.
282
283 =head1 HISTORY
284
285 2002-06-12 to Initial ISGTC release
286 2003-07-16 to Added -handle option
287 2003-07-29 to Added -param option
288
289 =head1 AUTHOR
290
291 S<Tobias Oetiker E<lt>oetiker@ee.ethz.chE<gt>>
292
293 =head1 COPYRIGHT
294
295 (C) 2000 by ETH Zurich
296
297 =head1 LICENSE
298
299 This code is made available under the GNU General Public License
300 Version 2.0 or later (see www.gnu.org)
301
302 =cut
303
304 package DBIx::PearlReports;
305
306 use Carp;
307 use strict;
308 use DBI;
309 use vars qw(%field $VERSION @EXPORT %EXPORT_TAGS @ISA); # it's a package global
310 require Exporter;
311 $VERSION=1.0;
312
313 @ISA = qw(Exporter);
314 @EXPORT = qw(%field rpmin rpmax rpsum rpcnt rpavg rpsmall rpbig);
315 %EXPORT_TAGS = ('Simple' => [qw(create group body makereport reset)],
316 'MyAgg' => [qw($aggmem)]);
317
318 #prototypes
319 sub argcheck ($$$$);
320 sub autoself (@);
321 sub ask ($;$);
322
323 my $DefaultSelf;
324 #implementation
325
326 sub create {
327 my %args = @_;
328 my $self = {};
329 bless $self;
330 $DefaultSelf = $self; # set default for SIMPLE use
331 if (not exists $args{-handle}){
332 argcheck "create", \%args, [qw(-datasource -query)],[qw(-username -password -param)];
333 $args{-username} = ask('Username:') unless defined $args{-username};
334 $args{-password} = ask('password:',1) unless defined $args{-password};
335 } else {
336 argcheck "create", \%args, [qw(-handle -query)],[(-param)];
337 $self->{dbh} = $args{-handle};
338 }
339 $self->{NEW} = \%args;
340 return $self;
341 }
342
343 sub new {
344 return create @_;
345 }
346
347 sub group {
348 my ($self, %args) = autoself @_;
349 argcheck "group", \%args, [qw(-trigger)],[qw(-head -foot)];
350 $args{aggmem} = {}; # this will hold info from the groups aggmem calls
351 push @{$self->{GROUPS}}, \%args;
352 }
353
354 sub body {
355 my ($self, %args) = autoself @_;
356 argcheck "body", \%args, [qw(-contents)],[];
357 croak "there can be only one body in a report"
358 if exists $self->{BODY}->{-contents};
359 $self->{BODY}->{-contents} = $args{-contents};
360 }
361
362 # Global variable providing static, group local memmory
363 # for all agregat functions.
364 my $aggmem;
365
366 # reset GROUPS and BODY asignement of report;
367
368 sub reset {
369 my ($self, %args) = autoself @_;
370 $self->{GROUPS} = [];
371 $self->{BODY} = undef;
372 }
373
374 sub makereport {
375 my ($self, %args) = autoself @_;
376 # open the database and get the data
377 if ( not defined $self->{dbh} ) {
378 $self->{dbh} = DBI->connect( $self->{NEW}->{-datasource},
379 $self->{NEW}->{-username},
380 $self->{NEW}->{-password}
381 ) or croak $DBI::errstr;
382 }
383
384 $self->{sth} = $self->{dbh}->prepare_cached($self->{NEW}->{-query})
385 or croak $self->{dbh}->errstr;
386
387 if ($self->{NEW}->{-param}){
388 my @param = @{$self->{NEW}->{-param}};
389 for(1..scalar(@param)){
390 #count from 1 to number_of_parameters including.
391 #sql parameters start at 1.
392 $self->{sth}->bind_param($_,shift @param);
393 }
394 }
395
396 $self->{sth}->execute() or croak $self->{dbh}->errstr."\n\nQuery: $self->{NEW}->{-query}";
397
398 my @report; #this array holds the report
399
400 # loop through query response
401 while (my $row = $self->{sth}->fetchrow_hashref) {
402 %field = %$row;
403 my $cascade;
404 my @headstack;
405 my @footstack;
406 foreach my $group (@{$self->{GROUPS}}) {
407 # $aggmem is the temporery storage are for all aggmem functions
408 # called within the current group
409
410 $aggmem = $group->{aggmem}; # asign aggmem pointer
411
412 #evaluate current value of the trigger functions
413 my $trigval = &{$group->{-trigger}};
414 if (defined $cascade or not defined $group->{trigval}
415 or $trigval ne $group->{trigval}){
416 $group->{trigval} = $trigval;
417
418 # if the trigger fired, fall through all the lower groups
419 # they have to rotate too
420 $cascade = 1;
421
422 $aggmem->{array} = []; # clear aggmem storage
423
424 unshift @footstack, $group->{footsave}
425 if defined $group->{-foot};
426 # OK, this is a bit of voodo. Because I want that you can use agregate
427 # functions in the head section we store a pointer to a string
428 # and push it into the report. For each record in the group this
429 # string gets updated (through the pointer)
430 my $string = "";
431 push @headstack, \$string;
432 $group->{headref} = \$string;
433 }
434 $aggmem->{counter} = 0; # reset aggmem storage pointer
435 ${$group->{headref}} = &{$group->{-head}} if exists $group->{-head};
436 $group->{footsave} = &{$group->{-foot}} if exists $group->{-foot};
437 }
438 push @report, @footstack;
439 push @report, @headstack;
440 push @report, &{$self->{BODY}->{-contents}} if defined $self->{BODY};
441
442 }
443 my @footstack;
444 foreach my $group (@{$self->{GROUPS}}) {
445 unshift @footstack , $group->{footsave}
446 if defined $group->{-foot};
447 }
448 push @report, @footstack;
449 $self->{sth}->finish;
450 # $self->{dbh}->disconnect;
451 # now we resolve all the -head pointers left in the report stack
452 # and we only ship values back which evaluated to a defined value
453 return grep {defined $_} map {ref $_ eq 'SCALAR' ? ${$_} : $_ } @report;
454 }
455
456 ##### Agregat functions ##############
457 # agregat functions can be used within the -foot and -head
458 # funtions. Before each call to either of these functions
459 # a persistent @stor and a to 0 initialized $count variable
460 # is made available with local
461
462 sub rpsum ($) {
463 my $cnt = $aggmem->{counter}++;
464 my $arr = \$aggmem->{array}->[$cnt];
465 $$arr += $_[0];
466 return $$arr;
467 }
468
469 sub rpmin ($) {
470 my $cnt = $aggmem->{counter}++;
471 my $arr = \$aggmem->{array}->[$cnt];
472 $$arr= $_[0]
473 if not defined $$arr
474 or $$arr > $_[0];
475 return $$arr;
476 }
477
478 sub rpmax ($) {
479 my $cnt = $aggmem->{counter}++;
480 my $arr = \$aggmem->{array}->[$cnt];
481 $$arr= $_[0]
482 if not defined $$arr
483 or $$arr < $_[0];
484 return $$arr;
485 }
486
487 sub rpsmall ($) {
488 my $cnt = $aggmem->{counter}++;
489 my $arr = \$aggmem->{array}->[$cnt];
490 $$arr= $_[0]
491 if not defined $$arr
492 or $$arr gt $_[0];
493 return $$arr;
494 }
495
496 sub rpbig ($) {
497 my $cnt = $aggmem->{counter}++;
498 my $arr = \$aggmem->{array}->[$cnt];
499 $$arr= $_[0]
500 if not defined $$arr
501 or $$arr lt $_[0];
502 return $$arr;
503 }
504
505 sub rpcnt ($) {
506 my $cnt = $aggmem->{counter}++;
507 my $arr = \$aggmem->{array}->[$cnt];
508 $$arr++;
509 return $$arr;
510 }
511
512 sub rpavg ($) {
513 my $cnt = $aggmem->{counter}++;
514 my $arr = \$aggmem->{array}->[$cnt];
515 $$arr->{sum} += $_[0];
516 $$arr->{cnt}++;
517 return $$arr->{sum} / $$arr->{cnt};
518 }
519
520 ##### Internal Helpers #################
521
522 sub ask ($;$) {
523 print STDERR $_[0]," ";
524 system "stty -echo"
525 if defined $_[1];
526 chomp(my $answer = <>);
527 if (defined $_[1]){
528 system "stty echo";
529 print "\n";
530 }
531 return $answer;
532 }
533
534 sub argcheck ($$$$) {
535 my $func = shift;
536 my $hash = shift;
537 my $required = shift;
538 my $optional = shift;
539 foreach my $arg (@{$required}) {
540 croak "$func expected $arg argument"
541 unless exists $hash->{$arg};
542 }
543 foreach my $arg (keys %{$hash}) {
544 croak "$func does not support $arg arguments"
545 unless grep /^$arg$/, @{$required}, @{$optional};
546 }
547 }
548
549 sub autoself (@){
550 return @_ if
551 ref($_[0]) eq 'DBIx::PearlReports';
552 return \$DefaultSelf, @_;
553 }
554
555 1;

  ViewVC Help
Powered by ViewVC 1.1.26