Re: pager

[prev] [thread] [next] [Date index for 2004/10/20]

From: David R. Baird
Subject: Re: pager
Date: 21:00 on 20 Oct 2004
--Message-Boundary-2842
Content-type: text/plain; charset=US-ASCII
Content-transfer-encoding: 7BIT
Content-description: Mail message body


I've taken Yuval's suggestion and made the pager easily subclassable. 
An example of a subclass is:

package Class::DBI::Plugin::Pager::LimitOffset;
use base 'Class::DBI::Plugin::Pager';
sub make_limit { "LIMIT $_[2] OFFSET $_[1]" }
1;


Again, any suggestions on additional syntaxes would be great. Current 
version is attached, I'll upload to CPAN as soon as I can write a few 
tests. 

Cheers,

d.


On 20 Oct 2004 at 14:53, David R. Baird wrote:

> 
> On 20 Oct 2004 at 12:41, Yuval Kogman wrote:
> 
> > On Wed, Oct 20, 2004 at 10:48:45 +0100, David R. Baird wrote:
> > Content-Description: Mail message body
> > > 
> > > Hi,
> > > 
> > > I'd be grateful for comments from the list on a pager module I've 
> > > written (attached to this email). It uses LIMIT so that only the 
> > > required subset of results are loaded into memory. I've tried to code 
> > > for different dialects, but I only run MySQL so I haven't tested the 
> > > others - any pointers would be great. 
> > 
> > With PG you do it just the same, with limit and offset. 
> 
> PostgreSQL and MySQL both use OFFSET and LIMIT, but the MySQL syntax 
> is different from PostgreSQL, at least in the older version of MySQL 
> I'm using. I'll check up on more recent versions. Seems like the 
> Postgre syntax has also changed between versions. 
> 
> > You should
> > note that ORDER BY should be considered mandatory, though:
> 
> Good point. Which also made me see I'd blocked users from specifying 
> their own order_by. I've changed it to accept an order_by clause in 
> the method call, and if there is none, to use the primary key 
> column(s). 
> 
> > With Oracle you have to use subqueries. Here is some info:
> 
> Subqueries can't be handled by this module, because it only has 
> access to the WHERE clause, not the whole SQL statement. 
> 
> > Please make it subclassable, so that you have a subclass per driver,
> > and then a subclass per method. Drivers are selcted by set_db
> > calling __driver, i guess you can get the data some how.
> > 
> > Then you basically have MySQL and Pg (and maybe others) inherit from
> > LimitOffset.pm, and others implementing their own.
> > 
> > A bit like Class::DBI::Loader...
> > 
> > It sounds a little overkill to do it this way but it's the most
> > flexible to extend, update, and reuse.
> 
> No, it's a great idea, I'm working on it. I'll send an updated 
> version to the list ASAP. I'm not clear on whether you see this 
> working at runtime or compile time? I'm thinking compile time like 
> this:
> 
> use Class::DBI::Plugin::Pager::mysql;
> 
> I'm no CDBI expert, what you say suggests that a CDBI app can have 
> several drivers, and these can be hooked up to different databases, 
> and the pager should be identifying the database and adapting to it? 
> 
> d.
> 
> 
> 
> -- 
> Dr. David R. Baird
> Riverside Content Management Systems
> http://www.riverside-cms.co.uk




        -- 
        Dr. David R. Baird
Riverside Content Management Systems
http://www.riverside-cms.co.uk


--Message-Boundary-2842
Content-type: text/plain; charset=US-ASCII
Content-transfer-encoding: 7BIT
Content-description: Text from file 'Pager.pm'

package Class::DBI::Plugin::Pager;
use strict;
use warnings;
use Carp;

use Data::Page;
use SQL::Abstract;

use vars '$VERSION';

$VERSION = 0.2;

my $Debug = 0;

sub import {
    my ( $class ) = @_;

    my $caller = caller(); # the CDBI app

    warn "CDBI::P::Pager caller is $caller" if $Debug;

    no strict 'refs';
    *{"$caller\::search_where_paged"}      = \&search_where_paged;
    *{"$caller\::search_where_limitable"}  = \&search_where_limitable;
    *{"$caller\::_make_limit_phrase"}      = $class->can( 'make_limit' );
}

sub make_limit {
    my ( undef, $offset, $rows ) = @_;

    require Class::DBI::Plugin::Pager::LimitOffset;

    return Class::DBI::Plugin::Pager::LimitOffset->make_limit( $offset, $rows );
}

# $where, [$attr], $per_page, [$page]

# or

# where     => $where,
# attr      => $attr,        # optional
# per_page  => $per_page,
# page      => $page         # optional

sub search_where_paged {
    my $class = shift;  # this is the plugged-into-class

    my ( $where, $attr, $per_page, $page );

    if ( ref $_[0] )
    {
        $where    = shift;
        $attr     = shift if ref $_[0];
        $per_page = shift || croak( 'no per_page argument' );
        $page     = shift || 1;
    }
    else
    {
        my %args  = @_;

        $where    = $args{where};
        $attr     = $args{attr};
        $per_page = $args{per_page} || croak( 'no per_page argument' );
        $page     = $args{page}     || 1;
    }

    $attr ||= {};

    my $count = $class->count_search_where( $where );

    my $pager = Data::Page->new( $count, $per_page, $page );

    # make sure the results are sorted by something
    my $order_by = $attr->{order_by} || $attr->{order} || [ $class->primary_columns ];

    my $limit = [ $pager->skipped, $pager->entries_per_page ];

    $attr = { limit    => $limit,
              order_by => $order_by,
              };

    my @results = $class->search_where_limitable( $where, $attr );

    return $pager, @results;
}

# This is CDBI::AbstractSearch::search_where, with extra limitations
sub search_where_limitable {
    my $class = shift;  # this is the plugged-into-class
    my $where = ( ref $_[0] ) ? $_[0] : { @_ };
    my $attr  = ( ref $_[0] ) ? $_[1] : undef;
    my $order = ( $attr ) ? delete( $attr->{order_by} ) : undef;
    my $limit = ( $attr ) ? delete( $attr->{limit} )    : undef;

    if ( $Debug )
    {
        require YAML;
        warn 'limitations: ' . YAML::Dump( $limit );
    }

    # order is deprecated, but still backward compatible
    if ( $attr && exists $attr->{order} )
    {
        $order = delete $attr->{order};
    }

    $class->can( 'retrieve_from_sql' ) or
        croak( "$class should inherit from Class::DBI >= 0.90" );

    my $sql = SQL::Abstract->new( %$attr );

    my ( $phrase, @bind ) = $sql->where( $where, $order );

    $phrase =~ s/^\s*WHERE\s*//i;

    if ( $limit )
    {
        my ( $offset, $rows, $limit_phrase );

        if ( my $input = ref $limit )
        {
            if ( $input eq 'ARRAY' )
            {
                ( $offset, $rows ) = @$limit;
            }
            elsif ( $input eq 'HASH' )
            {
                ( $offset, $rows ) = map { $limit->{ $_ } } qw( offset rows );
            }
            else
            {
                croak( "can't parse $limit for LIMIT params" );
            }

            croak( "undefined offset in $limit" ) unless defined $offset;
            croak( "undefined rows in $limit" )   unless defined $rows;

            $limit_phrase = $class->_make_limit_phrase( $offset, $rows );
        }
        else
        {
            $limit_phrase = $limit;
        }

        $phrase .= ' ' . $limit_phrase;

        warn "phrase: $phrase" if $Debug;
    }

    return $class->retrieve_from_sql( $phrase, @bind );
}

1;

__END__

=head1 NAME

Class::DBI::Plugin::Pager - paged results for CDBI::AbstractSearch

=head1 SYNOPSIS

    package CD;
    use base 'Class::DBI';

    # pager needs this
    use Class::DBI::Plugin::AbstractCount;

    use Class::DBI::Plugin::Pager;

    # or to use a different LIMIT syntax
    # use Class::DBI::Plugin::Pager::MySQL;

    __PACKAGE__->set_db(...);


    # in a nearby piece of code...

    use CD;

    # see SQL::Abstract and CDBI::AbstractSearch for how to specify the query
    my $where = { ... };

    my $attr => { order_by => 'foo' };

    my ( $pager, @cds ) = CD->search_where_paged( where    => $where,
                                                  attr     => $attr,
                                                  per_page => 10,
                                                  page     => 3,
                                                  );

    # or

    my ( $pager, @cds ) = CD->search_where_paged( $where, $attr, $per_page, $page );

    # $pager is a Data::Page object
    # @cds contains the CDs just for the current page

=head1 EXPORTS

The following methods are placed in the package of the caller (e.g. C<CD>).

=item search_where_paged

Accepts a L<SQL::Abstract|SQL::Abstract> query specification.

=item search_where_limitable

Accepts a L<SQL::Abstract|SQL::Abstract> query specification, and an additional
C<limit> spec.

    $where = { ... };

    $attr  = { order_by => [ 'this', 'that' ],
               limit    => [ $offset, $limit ],
               };

    CD->search_where_limitable( $where, $attr );

The C<limit> clause can be specified as an arrayref: C<[ $offset, $limit ]>,
a hashref: C<{ offset => $offset, rows => $rows }>, or a literal string:
C<LIMIT $rows OFFSET $offset>.

=item _make_limit_phrase

Puts the method C<_make_limit_phrase> into the package of the caller. This is
aliased to the C<make_limit> method of C<Class::DBI::Plugin::Pager> or a
subclass.


=head1 SUBCLASSES

This module works by appending a phrase to the end of the WHERE
clause generated by L<SQL::Abstract|SQL::Abstract>. Alternative syntaxes can
be implemented in subclasses. The default syntax works in PostgreSQL and
recent versions of MySQL, and probably some other databases.

Any comments on whether this actually does work for PostgreSQL
or InterBase would be very helpful, since I only have access to MySQL.

Subclasses should implement a C<make_limit> method. This receives three
arguments: the class of the CDBI package (e.g. 'CD'), and the C<offset> and <rows>
for the limit (or whatever) clause.

The subclasses implement the following LIMIT syntaxes:

=item Class::DBI::Plugin::Pager::LimitOffset

    LIMIT $rows OFFSET $offset

This is the default i.e. you can just say C<use Class::DBI::Plugin::Pager> to
get this syntax.

This should work for PostgreSQL, more recent MySQL, and some others.

=item Class::DBI::Plugin::LimitXY

    LIMIT $offset, $rows

I believe this syntax is only used by older versions of MySQL.

=item Class::DBI::Plugin::RowsTo

    ROWS $offset TO $offset + $rows

InterBase, also FireBird, maybe others?

=head1 TODO

I've only used this on an older version of MySQL. Reports of this thing
working (or not) elsewhere would be useful.

Tests.

=head1 CAVEATS

This class can't implement the subselect mechanism required by some databases
to emulate the LIMIT phrase, because it only has access to the WHERE clause,
not the whole SQL statement.

Each query issues two requests to the database - the first to count the entire
result set, the second to retrieve the required subset of results. If your
tables are small it may be quicker to use L<Class::DBI::Pager|Class::DBI::Pager>.

The C<order_by> clause means the database has to retrieve and sort the
entire results set, before chopping out the requested subset. It's probably a
good idea to have an index on the column(s) used to order the results.

=head1 DEPENDENCIES

L<SQL::Abstract|SQL::Abstract>,
L<Data::Page|Data::Page>,
L<Class::DBI::Plugin::AbstractCount|Class::DBI::Plugin::AbstractCount>,
L<Carp|Carp>.

=head1 SEE ALSO

L<Class::DBI::Pager|Class::DBI::Pager> does a similar job, but retrieves
the entire results set into memory before chopping out the page you want.

=head1 BUGS

Please report all bugs via the CPAN Request Tracker at
L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Class-DBI-Plugin-Pager>;.

=head1 COPYRIGHT AND LICENSE

Copyright 2004 by David Baird.

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

=head1 AUTHOR

David Baird, C<cpan@xxxxxxxxxxxxx.xx.xx>






--Message-Boundary-2842
Content-type: text/plain; charset=US-ASCII
Content-transfer-encoding: 7BIT
Content-description: Text from file 'RowsTo.pm'

package Class::DBI::Plugin::Pager::RowsTo;
use strict;
use warnings;

use base 'Class::DBI::Plugin::Pager';

sub make_limit {
    my ( undef, $offset, $rows ) = @_;

    my $last = $rows + $offset;

    return "ROWS $offset TO $last";
}

1;


--Message-Boundary-2842
Content-type: text/plain; charset=US-ASCII
Content-transfer-encoding: 7BIT
Content-description: Text from file 'LimitOffset.pm'

package Class::DBI::Plugin::Pager::LimitOffset;
use strict;
use warnings;

use base 'Class::DBI::Plugin::Pager';

sub make_limit {
    my ( undef, $offset, $rows ) = @_;

    return "LIMIT $rows OFFSET $offset";
}

1;


--Message-Boundary-2842
Content-type: text/plain; charset=US-ASCII
Content-transfer-encoding: 7BIT
Content-description: Text from file 'LimitXY.pm'

package Class::DBI::Plugin::Pager::LimitXY;
use strict;
use warnings;

use base 'Class::DBI::Plugin::Pager';

sub make_limit {
    my ( undef, $offset, $rows ) = @_;

    return "LIMIT $offset, $rows";
}

1;

--Message-Boundary-2842--

(message missing)

pager
David R. Baird 09:48 on 20 Oct 2004

Re: pager
David R. Baird 13:53 on 20 Oct 2004

Re: pager
David R. Baird 21:00 on 20 Oct 2004

Re: pager
merlyn (Randal L. Schwartz) 02:55 on 21 Oct 2004

Re: pager
David R. Baird 10:46 on 21 Oct 2004

Re: pager
David R. Baird 11:30 on 21 Oct 2004

Re: pager
merlyn (Randal L. Schwartz) 12:10 on 21 Oct 2004

Re: pager
David R. Baird 23:08 on 21 Oct 2004

Re: pager
Cees Hek 00:22 on 22 Oct 2004

Re: pager
Tony Bowden 08:34 on 22 Oct 2004

Re: pager
Emanuele Zeppieri 13:51 on 22 Oct 2004

Re: pager
David R. Baird 14:41 on 22 Oct 2004

Re: pager
David R. Baird 09:20 on 22 Oct 2004

Class::DBI modules not properly loaded under mod_perl ?
Michele Valzelli 10:55 on 22 Oct 2004

Re: pager
Emanuele Zeppieri 12:21 on 22 Oct 2004

Re: pager
David R. Baird 12:52 on 22 Oct 2004

Re: pager
Emanuele Zeppieri 15:33 on 22 Oct 2004

Re: pager
David R. Baird 16:09 on 22 Oct 2004

Re: pager
David R. Baird 23:30 on 07 Nov 2004

RE: pager
Emanuele Zeppieri 03:50 on 14 Nov 2004

Re: pager
Emanuele Zeppieri 13:25 on 22 Oct 2004

Generated at 11:35 on 01 Dec 2004 by mariachi v0.52