package CGI::Session::Driver::DBI;

# $Id$

use strict;

use DBI;
use Carp;
use CGI::Session::Driver;

@CGI::Session::Driver::DBI::ISA = ( "CGI::Session::Driver" );
$CGI::Session::Driver::DBI::VERSION = '4.43';


sub init {
    my $self = shift;
    if ( defined $self->{Handle} )  {
        if (ref $self->{Handle} eq 'CODE') {
            $self->{Handle} = $self->{Handle}->();
        }
        else {
            # We assume the handle is working, and there is nothing to do. 
        }
    }
    else {
        $self->{Handle} = DBI->connect( 
            $self->{DataSource}, $self->{User}, $self->{Password}, 
            { RaiseError=>1, PrintError=>1, AutoCommit=>1 }
        );
        unless ( $self->{Handle} ) {
            return $self->set_error( "init(): couldn't connect to database: " . DBI->errstr );
        }
        $self->{_disconnect} = 1;
    }
    return 1;
}

# A setter/accessor method for the table name, defaulting to 'sessions'

sub table_name {
    my $self = shift;
    my $class = ref( $self ) || $self;

    if ( (@_ == 0) && ref($self) && ($self->{TableName}) ) {
        return $self->{TableName};
    }

    no strict 'refs';
    if ( @_ ) {
        $self->{TableName} = shift;
    }

    unless (defined $self->{TableName}) {
        $self->{TableName} = "sessions";
    }

    return $self->{TableName};
}


sub retrieve {
    my $self = shift;
    my ($sid) = @_;
    croak "retrieve(): usage error" unless $sid;


    my $dbh = $self->{Handle};
    my $sth = $dbh->prepare_cached("SELECT $self->{DataColName} FROM " . $self->table_name . " WHERE $self->{IdColName}=?", undef, 3);
    unless ( $sth ) {
        return $self->set_error( "retrieve(): DBI->prepare failed with error message " . $dbh->errstr );
    }
    $sth->execute( $sid ) or return $self->set_error( "retrieve(): \$sth->execute failed with error message " . $sth->errstr);

    my ($row) = $sth->fetchrow_array();

    $sth->finish;

    return 0 unless $row;
    return $row;
}


sub store {
#    die;
    my $self = shift;
    my ($sid, $datastr) = @_;
    croak "store(): usage error" unless $sid && $datastr;


    my $dbh = $self->{Handle};
    my $sth = $dbh->prepare_cached("SELECT $self->{IdColName} FROM " . $self->table_name . " WHERE $self->{IdColName}=?", undef, 3);
    unless ( defined $sth ) {
        return $self->set_error( "store(): \$dbh->prepare failed with message " . $sth->errstr );
    }

    $sth->execute( $sid ) or return $self->set_error( "store(): \$sth->execute failed with message " . $sth->errstr );
    my $rc = $sth->fetchrow_array;
    $sth->finish;

    my $action_sth;
    if ( $rc ) {
        $action_sth = $dbh->prepare_cached("UPDATE " . $self->table_name . " SET $self->{DataColName}=? WHERE $self->{IdColName}=?", undef, 3);
    } else {
        $action_sth = $dbh->prepare_cached("INSERT INTO " . $self->table_name . " ($self->{DataColName}, $self->{IdColName}) VALUES(?, ?)", undef, 3);
    }
    
    unless ( defined $action_sth ) {
        return $self->set_error( "store(): \$dbh->prepare failed with message " . $dbh->errstr );
    }
    $action_sth->execute($datastr, $sid)
        or return $self->set_error( "store(): \$action_sth->execute failed " . $action_sth->errstr );

    $action_sth->finish;

    return 1;
}


sub remove {
    my $self = shift;
    my ($sid) = @_;
    croak "remove(): usage error" unless $sid;

   my $rc = $self->{Handle}->do( 'DELETE FROM ' . $self->table_name . " WHERE $self->{IdColName}= ?", {}, $sid );
    unless ( $rc ) {
        croak "remove(): \$dbh->do failed!";
    }
    
    return 1;
}


sub DESTROY {
    my $self = shift;

    unless ( defined $self->{Handle} && $self->{Handle} -> ping ) {
        $self->set_error(__PACKAGE__ . '::DESTROY(). Database handle has gone away');
        return;
	}

    unless ( $self->{Handle}->{AutoCommit} ) {
        $self->{Handle}->commit;
    }
    if ( $self->{_disconnect} ) {
        $self->{Handle}->disconnect;
    }
}


sub traverse {
    my $self = shift;
    my ($coderef) = @_;

    unless ( $coderef && ref( $coderef ) && (ref $coderef eq 'CODE') ) {
        croak "traverse(): usage error";
    }

    my $tablename = $self->table_name();
    my $sth = $self->{Handle}->prepare_cached("SELECT $self->{IdColName} FROM $tablename", undef, 3) 
        or return $self->set_error("traverse(): couldn't prepare SQL statement. " . $self->{Handle}->errstr);
    $sth->execute() or return $self->set_error("traverse(): couldn't execute statement $sth->{Statement}. " . $sth->errstr);

    while ( my ($sid) = $sth->fetchrow_array ) {
        $coderef->($sid);
    }

    $sth->finish;

    return 1;
}


1;

=pod

=head1 NAME

CGI::Session::Driver::DBI - Base class for native DBI-related CGI::Session drivers

=head1 SYNOPSIS

    require CGI::Session::Driver::DBI;
    @ISA = qw( CGI::Session::Driver::DBI );

=head1 DESCRIPTION

In most cases you can create a new DBI-driven CGI::Session driver by simply creating an empty driver file that inherits from CGI::Session::Driver::DBI. That's exactly what L<sqlite|CGI::Session::Driver::sqlite> does. The only reason why this class doesn't suit for a valid driver is its name isn't in lowercase. I'm serious!

=head2 NOTES

CGI::Session::Driver::DBI defines init() method, which makes DBI handle available for drivers in I<Handle> - object attribute regardless of what C<\%dsn_args> were used in creating session object. Should your driver require non-standard initialization you have to re-define init() method in your F<.pm> file, but make sure to set 'Handle' - object attribute to database handle (returned by DBI->connect(...)) if you wish to inherit any of the methods from CGI::Session::Driver::DBI.

=head1 STORAGE

Before you can use any DBI-based session drivers you need to make sure compatible database table is created for CGI::Session to work with. Following command will produce minimal requirements in most SQL databases:

    CREATE TABLE sessions (
        id CHAR(32) NOT NULL PRIMARY KEY,
        a_session TEXT NOT NULL
    );

Your session table can define additional columns, but the above two are required. Name of the session table is expected to be I<sessions> by default. You may use a different name if you wish. To do this you have to pass I<TableName> as part of your C< \%dsn_args >:

    $s = CGI::Session->new('driver:sqlite', undef, {TableName=>'my_sessions'});
    $s = CGI::Session->new('driver:mysql', undef,
    {
        TableName=>'my_sessions',
        DataSource=>'dbi:mysql:shopping_cart'.
    });

To use different column names, change the 'create table' statement, and then simply do this:

    $s = CGI::Session->new('driver:pg', undef,
    {
        TableName=>'session',
        IdColName=>'my_id',
        DataColName=>'my_data',
        DataSource=>'dbi:pg:dbname=project',
    });

or

    $s = CGI::Session->new('driver:pg', undef,
    {
        TableName=>'session',
        IdColName=>'my_id',
        DataColName=>'my_data',
        Handle=>$dbh,
    });

=head1 DRIVER ARGUMENTS

Following driver arguments are supported:

=over 4

=item DataSource

First argument to be passed to L<DBI|DBI>->L<connect()|DBI/connect()>. If the driver makes
the database connection itself, it will also explicitly disconnect from the database when 
the driver object is DESTROYed.

=item User

User privileged to connect to the database defined in C<DataSource>.

=item Password

Password of the I<User> privileged to connect to the database defined in C<DataSource>

=item Handle

An existing L<DBI> database handle object. The handle can be created on demand
by providing a code reference as a argument, such as C<<sub{DBI->connect}>>.
This way, the database connection is only created if it actually needed. This can be useful
when combined with a framework plugin like L<CGI::Application::Plugin::Session>, which creates
a CGI::Session object on demand as well. 

C<Handle> will override all the above arguments, if any present.

=item TableName

Name of the table session data will be stored in.

=back

=head1 LICENSING

For support and licensing information see L<CGI::Session|CGI::Session>

=cut