File Coverage

blib/lib/Class/DBI/Plugin/Pager.pm
Criterion Covered Total %
statement 33 149 22.1
branch 2 60 3.3
condition 0 60 0.0
subroutine 9 22 40.9
pod 9 9 100.0
total 53 300 17.6


line stmt bran cond sub pod time code
1             package Class::DBI::Plugin::Pager;
2 2     2   19752 use strict;
  2         4  
  2         68  
3 2     2   11 use warnings;
  2         4  
  2         68  
4 2     2   10 use Carp;
  2         13  
  2         184  
5              
6 2     2   1200 use UNIVERSAL::require;
  2         2809  
  2         21  
7 2     2   1862 use SQL::Abstract;
  2         19701  
  2         111  
8              
9 2     2   15 use base qw( Data::Page Class::Data::Inheritable );
  2         2  
  2         979  
10              
11 2     2   8554 use vars qw( $VERSION );
  2         4  
  2         285  
12              
13             $VERSION = '0.6_4';
14              
15             # D::P inherits from Class::Accessor::Chained::Fast
16             __PACKAGE__->mk_accessors( qw( _where abstract_attr per_page page _order_by _cdbi_app ) );
17              
18             __PACKAGE__->mk_classdata( '_syntax' );
19             __PACKAGE__->mk_classdata( '_pager_class' );
20              
21              
22             =head1 NAME
23              
24             Class::DBI::Plugin::Pager - paged queries for CDBI
25              
26             =head1 DESCRIPTION
27              
28             Adds a pager method to your class that can query using SQL::Abstract where clauses,
29             and limit the number of rows returned to a specific subset.
30              
31             =head1 SYNOPSIS
32              
33             package CD;
34             use base 'Class::DBI';
35              
36             use Class::DBI::Plugin::AbstractCount; # pager needs this
37            
38             use Class::DBI::Plugin::Pager;
39              
40             # or to use a different syntax
41             # use Class::DBI::Plugin::Pager::RowsTo;
42              
43             __PACKAGE__->set_db(...);
44              
45              
46             # in a nearby piece of code...
47              
48             use CD;
49              
50             # see SQL::Abstract for how to specify the query
51             my $where = { ... };
52              
53             my $order_by => [ qw( foo bar ) ];
54              
55             # bit by bit:
56             my $pager = CD->pager;
57              
58             $pager->per_page( 10 );
59             $pager->page( 3 );
60             $pager->where( $where );
61             $pager->order_by( $order_by );
62              
63             $pager->set_syntax( 'RowsTo' );
64              
65             my @cds = $pager->search_where;
66              
67             # or all at once
68             my $pager = CD->pager( $where, $order_by, 10, 3 );
69              
70             my @cds = $pager->search_where;
71              
72             # or
73              
74             my $pager = CD->pager;
75              
76             my @cds = $pager->search_where( $where, $order_by, 10, 3 );
77              
78             # $pager isa Data::Page
79             # @cds contains the CDs just for the current page
80              
81             =head1 METHODS
82              
83             =over
84              
85             =item import
86              
87             Loads the C method into the CDBI app.
88              
89             =cut
90              
91             sub import {
92 1     1   13 my ( $class ) = @_; # the pager class or subclass
93              
94 1         3 __PACKAGE__->_pager_class( $class );
95              
96 1         6 my $caller;
97              
98             # find the app
99 1         3 foreach my $level ( 0 .. 10 )
100             {
101 1         1 $caller = caller( $level );
102 1 50       7 last if UNIVERSAL::isa( $caller, 'Class::DBI' )
103             }
104              
105 1 50       3 croak( "can't find the CDBI app" ) unless $caller;
106              
107 2     2   10 no strict 'refs';
  2         3  
  2         3149  
108 1         1 *{"$caller\::pager"} = \&pager;
  1         15  
109             }
110              
111             =item pager( [$where, [$abstract_attr]], [$order_by], [$per_page], [$page], [$syntax] )
112              
113             Also accepts named arguments:
114              
115             where => $where,
116             abstract_attr => $attr,
117             order_by => $order_by,
118             per_page => $per_page,
119             page => $page,
120             syntax => $syntax
121              
122             Returns a pager object. This subclasses L.
123              
124             Note that for positional arguments, C<$abstract_attr> can only be passed if
125             preceded by a C<$where> argument.
126              
127             C<$abstract_attr> can contain the C<$order_by> setting (just as in
128             L).
129              
130             =over 4
131              
132             =item configuration
133              
134             The named arguments all exist as get/set methods.
135              
136             =over 4
137              
138             =item where
139              
140             A hashref specifying the query. See L.
141              
142             =item abstract_attr
143              
144             A hashref specifying extra options to be passed through to the
145             L constructor.
146              
147             =item order_by
148              
149             Single column name or arrayref of column names for the ORDER BY clause.
150             Defaults to the primary key(s) if not set.
151              
152             =item per_page
153              
154             Number of results per page.
155              
156             Defaults to 10, b if using the positional
157             arguments style, and supplying the C attribute, C must also
158             be supplied.
159              
160             =item page
161              
162             The pager will retrieve results just for this page. Defaults to 1.
163              
164             =item syntax
165              
166             Change the way the 'limit' clause is constructed. See C. Default
167             is C.
168              
169             =back
170              
171             =back
172              
173             =cut
174              
175             sub pager {
176 0     0 1   my $cdbi = shift;
177              
178 0           my $class = __PACKAGE__->_pager_class;
179              
180 0           my $self = bless {}, $class;
181              
182 0           $self->_cdbi_app( $cdbi );
183              
184             # This has to come before _init, so the caller can choose to set the syntax
185             # instead. But don't auto-set if we're a subclass.
186 0 0         $self->auto_set_syntax if $class eq __PACKAGE__;
187              
188 0           $self->_init( @_ );
189              
190 0           return $self;
191             }
192              
193             # _init is also called by results, so preserve any existing settings if
194             # new settings are not provided
195             sub _init {
196 0     0     my $self = shift;
197              
198 0 0         return unless @_;
199              
200 0           my ( $where, $abstract_attr, $order_by, $per_page, $page, $syntax, $named_args );
201            
202             # I wish I'd never implemented positional arguments in the first place!
203             # Does anyone use this?
204            
205 0 0         if ( @_ % 2 == 0 )
206             { # _might_ be named args
207 0           my %args = @_;
208            
209 0           $where = $args{where};
210 0           $abstract_attr = $args{abstract_attr};
211 0           $order_by = $args{order_by};
212 0           $per_page = $args{per_page};
213 0           $page = $args{page};
214 0           $syntax = $args{syntax};
215              
216 0   0       $named_args = $where || $abstract_attr || $order_by
217             || $per_page || $page || $syntax;
218             }
219            
220             #if ( ref( $_[0] ) or $_[0] =~ /^\d+$/ )
221 0 0         unless ( $named_args )
222             {
223 0 0         $where = shift if ref $_[0]; # SQL::Abstract accepts a hashref or an arrayref
224 0 0         $abstract_attr = shift if ref $_[0] eq 'HASH';
225 0 0 0       $order_by = shift if ( @_ and $_[0] !~ /^\d+$/ );
226 0 0 0       $per_page = shift if ( @_ and $_[0] =~ /^\d+$/ );
227 0 0 0       $page = shift if ( @_ and $_[0] =~ /^\d+$/ );
228 0           $syntax = shift;
229             }
230              
231             # Emulate AbstractSearch's search_where ordering -VV 20041209
232 0 0 0       $order_by = delete $$abstract_attr{order_by} if ($abstract_attr and !$order_by);
233            
234 0   0       $per_page ||= $self->per_page;
235 0   0       $page ||= $self->page;
236              
237 0   0       $self->per_page( $per_page || 10 ); # if $per_page;
238 0 0         $self->set_syntax( $syntax ) if $syntax;
239 0 0         $self->abstract_attr( $abstract_attr )if $abstract_attr;
240 0 0         $self->where( $where ) if $where;
241 0 0         $self->order_by( $order_by ) if $order_by;
242 0   0       $self->page( $page || 1 ); # if $page;
243             }
244              
245             =item where( [ $where ] )
246              
247             Translates accessor names into column names (or is it vice versa?). The
248             original C<$where> can be retrieved via C<_where()>.
249              
250             =cut
251              
252             sub where {
253 0     0 1   my ( $self, $where ) = @_;
254            
255 0   0       $where ||= $self->_where;
256            
257 0           $self->_where( $where );
258            
259 0           my %columns = $self->_real_col_names( keys %$where );
260            
261             # keys are candidates, values are real names
262 0           my %munged_where = map { $columns{ $_ } => $where->{ $_ } } keys %columns;
  0            
263            
264 0           return \%munged_where;
265             }
266              
267             =item order_by( [ $order_by ] )
268              
269             Does the same translation as C.
270              
271             =cut
272              
273             sub order_by {
274 0     0 1   my ( $self, $order_by ) = @_;
275            
276 0   0       $order_by ||= $self->_order_by;
277            
278 0           $self->_order_by( $order_by );
279            
280 0 0         my @candidates = ref( $order_by ) ? @$order_by :
    0          
281             $order_by ? ( $order_by ) : ();
282              
283 0           my %columns = $self->_real_col_names( @candidates );
284            
285 0           return [ map { "$_" } values %columns ];
  0            
286             }
287              
288             sub _real_col_names {
289 0     0     my ( $self, @candidates ) = @_;
290            
291 0           my $cdbi = $self->_cdbi_app;
292            
293 0           my %cols = ();
294            
295 0           foreach my $candidate ( @candidates )
296             {
297             # lifted from Class::DBI::Plugin::CountSearch
298             my $column = $cdbi->find_column( $candidate )
299 0   0 0     || ( List::Util::first { $_->accessor eq $candidate } $cdbi->columns )
  0            
300             || $cdbi->_croak("'$candidate' is not a column of $cdbi");
301            
302 0           $cols{ $candidate } = $column;
303             }
304            
305 0           return %cols;
306             }
307              
308             =item add_attr( %attributes )
309              
310             Add to (or override) the L attributes:
311              
312             $pager->add_attr( cmp => 'like' );
313            
314             =cut
315              
316             sub add_attr {
317 0     0 1   my ( $self, %attr ) = @_;
318            
319 0           my $abs_attr = $self->abstract_attr;
320            
321 0           $abs_attr->{ $_ } = $attr{ $_ } for keys %attr;
322            
323 0           $self->abstract_attr( $abs_attr );
324             }
325            
326              
327             =item search_where
328              
329             Retrieves results from the pager. Accepts the same arguments as the C
330             method.
331              
332             =cut
333              
334             # like CDBI::AbstractSearch::search_where, with extra limitations
335             sub search_where {
336 0     0 1   my $self = shift;
337              
338 0           $self->_init( @_ );
339              
340 0           $self->_setup_pager;
341              
342 0           my $cdbi = $self->_cdbi_app;
343              
344 0   0       my $order_by = $self->order_by || [ $cdbi->primary_columns ];
345 0           my $where = $self->where;
346 0   0       my $syntax = $self->_syntax || $self->set_syntax;
347 0           my $limit_phrase = $self->$syntax;
348 0 0         my $sql = SQL::Abstract->new( %{ $self->abstract_attr || {} } );
  0            
349              
350 0 0         $order_by = [ $order_by ] unless ref $order_by;
351 0           my ( $phrase, @bind ) = $sql->where( $where, $order_by );
352              
353 0           $phrase .= ' ' . $limit_phrase;
354 0           $phrase =~ s/^\s*WHERE\s*//i;
355              
356 0           return $cdbi->retrieve_from_sql( $phrase, @bind );
357             }
358              
359             =item retrieve_all
360              
361             Convenience method, generates a WHERE clause that matches all rows from the table.
362              
363             Accepts the same arguments as the C or C methods, except that no
364             WHERE clause should be specified.
365              
366             Note that the argument parsing routine called by the C method cannot cope with
367             positional arguments that lack a WHERE clause, so either use named arguments, or the
368             'bit by bit' approach, or pass the arguments directly to C.
369              
370             =cut
371              
372             sub retrieve_all {
373 0     0 1   my $self = shift;
374              
375 0           my $get_all = { 1 => 1 };
376              
377 0 0         unless ( @_ )
378             { # already set pager up via method calls
379 0           $self->where( $get_all );
380 0           return $self->search_where;
381             }
382            
383 0 0 0       my @args = ( ref( $_[0] ) or $_[0] =~ /^\d+$/ ) ?
384             ( $get_all, @_ ) : # send an array
385             ( where => $get_all, @_ ); # send a hash
386              
387 0           return $self->search_where( @args );
388             }
389              
390             =item retrieve_all_sorted_by( $order )
391              
392             Useful for L.
393              
394             =cut
395              
396             sub retrieve_all_sorted_by {
397 0     0 1   my ( $self, $order ) = @_;
398            
399 0           return $self->retrieve_all( { order_by => $order } );
400             }
401              
402             sub _setup_pager {
403 0     0     my ( $self ) = @_;
404              
405 0   0       my $where = $self->where || croak( 'must set a query before retrieving results' );
406 0   0       my $per_page = $self->per_page || croak( 'no. of entries per page not specified' );
407 0           my $cdbi = $self->_cdbi_app;
408 0           my $count = $cdbi->count_search_where( $where, $self->abstract_attr );
409             #my $count = $self->_get_count;
410 0   0       my $page = $self->page || 1;
411              
412 0           $self->total_entries( $count );
413 0           $self->entries_per_page( $per_page );
414 0           $self->current_page( $page );
415            
416 0 0         croak( 'Fewer than one entry per page!' ) if $self->entries_per_page < 1;
417              
418 0 0         $self->current_page( $self->first_page ) unless defined $self->current_page;
419 0 0         $self->current_page( $self->first_page ) if $self->current_page < $self->first_page;
420 0 0         $self->current_page( $self->last_page ) if $self->current_page > $self->last_page;
421             }
422              
423              
424              
425             #sub _get_count {
426             # my ( $self ) = @_;
427             #
428             # my $where = $self->where || croak( 'must set a query before retrieving results' );
429             # my $cdbi = $self->_cdbi_app;
430             #
431             # # Class::DBI::Plugin::AbstractCount can handle SQL::Abstract attributes
432             # return $cdbi->count_search_where( $where, $self->abstract_attr )
433             # if $cdbi->can( 'count_search_where' );
434             #
435             # # Class::DBI::Plugin::CountSearch correctly handles aliased column accessors
436             # # (i.e. accessors for columns where the accessor has a different name from the
437             # # column). But it can't handle SQL::Abstract attributes.
438             # croak( 'no way to count total entries' ) unless $cdbi->can( 'count_search' );
439             #
440             # croak( 'Class::DBI::Plugin::CountSearch does not handle SQL::Abstract attributes - '
441             # . 'use Class::DBI::Plugin::AbstractCount instead' )
442             # if $self->abstract_attr;
443             #
444             # my @cols = keys %$where;
445             #
446             # croak('no keys in where clause') unless @cols > 0;
447             #
448             # # unqualified count
449             # return $cdbi->count_search if ( @cols == 1 and $cols[0] eq '1' );
450             #
451             # # count with search fields
452             # my @args;
453             #
454             # foreach my $col ( @cols )
455             # {
456             # my $expr = $where->{$col};
457             #
458             # croak( "code doesn't handle anything but 'like'!" )
459             # unless keys %$expr == 1 and $expr->{like};
460             #
461             # push @args, $col => $expr->{like};
462             # }
463             #
464             # return $cdbi->count_search_like( @args );
465             #}
466              
467             # SQL::Abstract::_recurse_where eats the WHERE clause
468             #sub where {
469             # my ( $self, $where_ref ) = @_;
470             #
471             # return $self->_where unless $where_ref;
472             #
473             # my $where_copy;
474             #
475             # if ( ref( $where_ref ) eq 'HASH' ) {
476             # $where_copy = { %$where_ref };
477             # }
478             # elsif ( ref( $where_ref ) eq 'ARRAY' )
479             # {
480             # $where_copy = [ @$where_ref ];
481             # }
482             # else
483             # {
484             # die "WHERE clause [$where_ref] must be specified as an ARRAYREF or HASHREF";
485             # }
486             #
487             # # this will get eaten, but the caller's value is now protected
488             # $self->_where( $where_copy );
489             #}
490              
491             =item set_syntax( [ $name || $class || $coderef ] )
492              
493             Changes the syntax used to generate the C or other phrase that restricts
494             the results set to the required page.
495              
496             The syntax is implemented as a method called on the pager, which can be
497             queried to provide the C<$rows> and C<$offset> parameters (see the subclasses
498             included in this distribution).
499              
500             =over 4
501              
502             =item $class
503              
504             A class with a C method.
505              
506             =item $name
507              
508             Name of a class in the C namespace, which has a
509             C method.
510              
511             =item $coderef
512              
513             Will be called as a method on the pager object, so receives the pager as its
514             argument.
515              
516             =item (no args)
517              
518             Called without args, will default to C, which causes
519             L
520             to be used.
521              
522             =back
523              
524             =cut
525              
526             sub set_syntax {
527 0     0 1   my ( $proto, $syntax ) = @_;
528              
529             # pick up default from subclass, or load from LimitOffset
530 0   0       $syntax ||= $proto->can( 'make_limit' );
531 0   0       $syntax ||= 'LimitOffset';
532              
533 0 0         if ( ref( $syntax ) eq 'CODE' )
534             {
535 0           $proto->_syntax( $syntax );
536 0           return $syntax;
537             }
538              
539 0 0         my $format_class = $syntax =~ '::' ? $syntax : "Class::DBI::Plugin::Pager::$syntax";
540              
541 0 0         $format_class->require || croak "error loading $format_class: $UNIVERSAL::require::ERROR";
542              
543 0   0       my $formatter = $format_class->can( 'make_limit' ) || croak "no make_limit method in $format_class";
544              
545 0           $proto->_syntax( $formatter );
546              
547 0           return $formatter;
548             }
549              
550             =item auto_set_syntax
551              
552             This is called automatically when you call C, and attempts to set the
553             syntax automatically.
554              
555             If you are using a subclass of the pager, this method will not be called.
556              
557             Will C if using Oracle or DB2, since there is no simple syntax for limiting
558             the results set. DB2 has a C keyword, but that seems to apply to a
559             cursor and I don't know if there is a cursor available to the pager. There
560             should probably be others to add to the unsupported list.
561              
562             Supports the following drivers:
563              
564             DRIVER CDBI::P::Pager subclass
565             my %supported = ( pg => 'LimitOffset',
566             mysql => 'LimitOffset', # older versions need LimitXY
567             sqlite => 'LimitOffset', # or LimitYX
568             interbase => 'RowsTo',
569             firebird => 'RowsTo',
570             );
571              
572             Older versions of MySQL should use the LimitXY syntax. You'll need to set it
573             manually, either by C, or by passing
574             C 'LimitXY'> to a method call, or call C directly.
575              
576             Any driver not in the supported or unsupported lists defaults to LimitOffset.
577              
578             Any additions to the supported and unsupported lists gratefully received.
579              
580             =cut
581              
582             sub auto_set_syntax {
583 0     0 1   my ( $self ) = @_;
584              
585             # not an exhaustive list
586 0           my %not_supported = ( oracle => 'Oracle',
587             db2 => 'DB2',
588             );
589              
590             # additions welcome
591 0           my %supported = ( pg => 'LimitOffset',
592             mysql => 'LimitOffset', # older versions need LimitXY
593             sqlite => 'LimitOffset', # or LimitYX
594             interbase => 'RowsTo',
595             firebird => 'RowsTo',
596             );
597              
598 0           my $cdbi = $self->_cdbi_app;
599              
600 0           my $driver = lc( $cdbi->__driver );
601              
602             die __PACKAGE__ . " can't build limit clauses for $not_supported{ $driver }"
603 0 0         if $not_supported{ $driver };
604              
605 0   0       $self->set_syntax( $supported{ $driver } || 'LimitOffset' );
606             }
607              
608             1;
609              
610             __END__