{ # CBNP::DBI::ErrorHandler scope
package CBNP::DBI::ErrorHandler;

use strict;
use vars qw($VERSION);

use Carp;
use DBI;

use CBNP::Constants ":standard";

$VERSION = "0.01";

# POD-formatted documentation
#-------------------------------------------------------------------------------

=head1 NAME

CBNP::DBI::ErrorHandler - Insulation for using DBI

=head1 SYNOPSIS

  use CBNP::DBI::ErrorHandler;

  # You may want to use CBNP::MetaLite to get your database connection instead
  # of doing it yourself like this
  my $bufferedHandle = 
    CBNP::DBI::ErrorHandler->connect(...DBI connection parameters...);

  # These DBI::db functions are currently insulated with ErrorHandler::db 
  $bufferedHandle->do($sqlString);
  @answerList = $bufferedHandle->selectrow_array($sqlString);
  $answerListRef = $bufferedHandle->selectall_arrayref($sqlString);
  $buffStatement = $bufferedHandle->prepare($sqlString);
  $bufferedHandle->commit();

  # These functions provided by ErrorHandler::db, but are not part of DBI
  @answerList = $bufferedHandle->selectrow_array_exists($sqlString);
  $answerListRef = $bufferedHandle->selectall_arrayref_exists($sqlString);

  # These DBI::st functions are currently insulated with ErrorHandler::st 
  $buffStatement->execute();
  $answerRowRef = $buffStatement->fetchrow_arrayref();
  @answerRow = $buffStatement->fetchrow_array();
  $answerListRef = $buffStatement->fetchall_arrayref();

  # This function is provided by ErrorHandler::st, but is not part of DBI
  $answerListRef = $buffStatement->fetchall_arrayref_exists();

=head1 EXAMPLES

See synopsis.

=head1 DESCRIPTION

CBNP::DBI::ErrorHandler takes care of some mundane error handling.  If you
use it directly, it will hopefully clean up your code some.

The MORE IMPORTANT use of the error handler is through a subclass, like
CBNP::DBI::ErrorHandler::Retry. 

CBNP::DBI::ErrorHandler::Retry implements error retries for a set of 
acceptable errors.  For example, when database congestion is high and a
statement encounters a deadlock situation, it will automatically retry.

=head1 APPENDIX

The following documentation describes the functions in this package

=cut

#-------------------------------------------------------------------------------

=head2 connect 

 Usage     : $buffHandle = CBNP::DBI::ErrorHandler->connect(
               $dataSource, $userName, $password);
             $buffHandle = CBNP::DBI::ErrorHandler->connect(
	       $dataSource, $userName, $password, \%attributes);  
 Function  : Execute's DBI's connect() function.  Will throw an exception
             if there is a problem connecting to the data source. 
 Arguments : String - data source string
             String - user name for the database login
	     String - user password for the database login
	     HashRef (optional) - attribute ref to be passed to DBI 
 Example   : See Usage 
 Returns   : CBNP::DBI::ErrorHandler::db - database handle ready for use

=cut

sub connect
{
  my ($this, $dataSourceString, $userName, $password, $DBIParamRef) = @_;

  my $dbHandle = DBI->connect($dataSourceString, 
                              $userName, 
			      $password, 
			      $DBIParamRef);
  if(defined DBI::err())
  {
    confess("failed to connect to data source \"$dataSourceString\".");
  }

  # Re-bless the handle into this class type
  bless($dbHandle, "CBNP::DBI::ErrorHandler::db");
 
  return $dbHandle;   
}

} # End CBNP::DBI::ErrorHandler scope

{ # Begin CBNP::DBI::ErrorHandler::db scope
package CBNP::DBI::ErrorHandler::db;

use strict;
use vars qw($VERSION @ISA);

use Carp;
use DBI;

use CBNP::Constants ":standard";

$VERSION = "0.01";
@ISA = ("DBI::db");

#-------------------------------------------------------------------------------

=head2 do 

 Usage     : $buffHandle->do($sqlString);
             $buffHandle->do($sqlString, \%attributes);
             $buffHandle->do($sqlString, \%attributes, @bindValues); 
 Function  : Executes DBI's do() on the database handle, will throw an 
             exception if the statement returns abnormally
 Arguments : String - SQL string to be executed
             Hash ref (optional) - Attribute ref to be passed to DBI
             String array (optional) - Bind values for the SQL statement 
 Example   : See Usage
 Returns   : <n/a>

=cut

sub do
{
  my ($this, $sqlString, $attributeRef, @bindValues) = @_;
  
  my $affectedRowCount = 
    $this->SUPER::do($sqlString, $attributeRef, @bindValues);

  if(defined $this->err())
  {
    confess("do() failed for SQL \"$sqlString\": " . DBI::errstr()); 
  }

  return $affectedRowCount;
}

#-------------------------------------------------------------------------------

=head2 selectrow_array

 Usage     : @row = $buffHandle->selectrow_array($sqlString);
             @row = $buffHandle->selectrow_array($sqlString, 
                                                 \%attributes);
             @row = $buffHandle->selectrow_array($sqlString, 
                                                 \%attributes,  
                                                 @bindValues); 
 Function  : Executes DBI's selectrow_array() on the database handle, will
             throw an exception if the statement returns abnormally 
 Arguments : String - SQL string to be executed
             Hash ref (optional) - Attribute ref to be passed to DBI
             String array (optional) - Bind values for the SQL statement 
 Example   : See Usage
 Returns   : Array - the row of data requested 

=cut

sub selectrow_array
{
  my ($this, $sqlString, $attributeRef, @bindValues) = @_;

  # Orignially this copied the array an extra time so I could catch
  # an abnormal return from selectrow_array. This didn't work, since the 
  # after-clause "OR" was changing the return value of the selectrow_array
  # (with the "OR" it would return only the first column of the first row).
  # I'm not sure why this was happening.
  # Now, abnormal return is assumed only if DBI::err is defined, which seems
  # to be the only reasonable way (using RaiseError, or checking for an
  # empty return list seem un-clean) to detect it.

  # I don't like that this will copy the array an extra time - sorry
  my @answer = $this->SUPER::selectrow_array($sqlString,
                                             $attributeRef,
                                             @bindValues);

  if(defined $this->err())
  {
    confess("selectrow_array() failed for SQL \"$sqlString\": " .
            DBI::errstr());
  }

  return wantarray ? @answer : $answer[0];
}

#-------------------------------------------------------------------------------

=head2 selectrow_array_exists

 Usage     : @row = $buffHandle->selectrow_array_exists($sqlString);
             @row = $buffHandle->selectrow_array_exists($sqlString, 
                                                        \%attributes);
             @row = $buffHandle->selectrow_array_exists($sqlString, 
                                                        \%attributes,  
                                                        @bindValues); 
 Function  : Executes DBI's selectrow_array() on the database handle, will
             throw an exception if no data is returned, or if the statement 
	     returns abnormally. Use this when you NEED values returned.
 Arguments : String - SQL string to be executed
             Hash ref (optional) - Attribute ref to be passed to DBI
             String array (optional) - Bind values for the SQL statement 
 Example   : See Usage
 Returns   : Array - the row of data requested 

=cut

sub selectrow_array_exists
{
  my ($this, $sqlString, $attributeRef, @bindValues) = @_;

  # I don't like that this will copy the array an extra time - sorry
  my @answer = $this->SUPER::selectrow_array($sqlString,
                                             $attributeRef,
                                             @bindValues);
  
  if(defined $this->err())
  {
      confess("selectrow_array_exists() failed for SQL \"$sqlString\": " .
              DBI::errstr());
  }

  # Additional check for existence since I'm not using the empty list
  # to detect an error, only the err() status.
  if(scalar(@answer) == 0 ||
     !defined $answer[0])
  {
    confess("selectrow_array_exists() resulted in no data for SQL " .
            "\"$sqlString\"");
  }

  return wantarray ? @answer : $answer[0];
}

#-------------------------------------------------------------------------------

=head2 selectall_arrayref 

 Usage     : $listRef = $buffHandle->selectall_arrayref($sqlString);
             $listRef = $buffHandle->selectall_arrayref($sqlString,
                                                        \%attributes);
             $listRef = $buffHandle->selectall_arrayref($sqlString,
                                                        \%attributes,
                                                        @bindValues);
 Function  : Executes DBI's selectall_arrayref() on the database handle, and
             will throw an exception if it returns abnormally
 Arguments : String - SQL string to be executed
             Hash ref (optional) - Attribute ref to be passed to DBI
             String array (optional) - Bind values for the SQL statement 
 Example   : See Usage
 Returns   : Array ref - reference to the array of rows retrieved 

=cut

sub selectall_arrayref
{
  my ($this, $sqlString, $attributeRef, @bindValues) = @_;

  my $answerRef = $this->SUPER::selectall_arrayref($sqlString,
                                                   $attributeRef,
                                                   @bindValues);
  if(defined $this->err())
  {
    confess("selectall_arrayref() failed for SQL \"$sqlString\": " .
            DBI::errstr());
  }

  return $answerRef;
}

#-------------------------------------------------------------------------------

=head2 selectall_arrayref_exists 

 Usage     : $listRef = $buffHandle->selectall_arrayref_exists($sqlString);
             $listRef = $buffHandle->selectall_arrayref_exists($sqlString,
                                                               \%attributes);
             $listRef = $buffHandle->selectall_arrayref_exists($sqlString,
                                                               \%attributes,
                                                               @bindValues);
 Function  : Executes DBI's selectall_arrayref() on the database handle.  Will
             and will throw an exception if no data is returned, or if it 
             returns abnormally. Use this when you NEED values returned.
 Arguments : String - SQL string to be executed
             Hash ref (optional) - Attribute ref to be passed to DBI
             String array (optional) - Bind values for the SQL statement 
 Example   : See Usage
 Returns   : Array ref - reference to the array of rows retrieved 

=cut

sub selectall_arrayref_exists
{
  my ($this, $sqlString, $attributeRef, @bindValues) = @_;

  my $answerRef = $this->SUPER::selectall_arrayref($sqlString,
                                                   $attributeRef,
                                                   @bindValues);
  if(defined $this->err())
  {
    confess("selectall_arrayref_exists() failed for SQL " .
            "\"$sqlString\": " .  DBI::errstr());
  }

  if(!defined $answerRef ||
     !defined $answerRef->[0] ||
     !defined $answerRef->[0]->[0])
  {
    confess("selectall_arrayref_exists() resulted in no data for SQL " .
            "\"$sqlString\"");
  }

  return $answerRef;
}

#-------------------------------------------------------------------------------

=head2 prepare 

 Usage     : $DBIStatementHandle = $buffHandle->prepare($sqlString);
             $DBIStatementHandle = $buffHandle->prepare($sqlString,
                                                        \%attributes);
 Function  : Executes DBI's prepare() on the database handle  
 Arguments : String - SQL string to be executed
             Hash ref (optional) - Attribute ref to be passed to DBI
 Example   : See Usage
 Returns   : CBNP::DBI::ErrorHandler::Retry::st - statement handle that is 
             retuned from the call to prepare() 

=cut

sub prepare
{
  my ($this, $sqlString, $attributeRef) = @_;

  my $statementHandle = $this->SUPER::prepare($sqlString, $attributeRef);

  if(defined $this->err())
  {
    confess("prepare() failed for SQL \"$sqlString\": " . DBI::errstr());
  }

  bless($statementHandle, "CBNP::DBI::ErrorHandler::st");

  return $statementHandle;
}

#-------------------------------------------------------------------------------

=head2 commit 

 Usage     : $buffHandle->commit(); 
 Function  : Executes DBI's commit() on the database handle 
 Arguments : <n/a> 
 Example   : See Usage
 Returns   : <n/a>

=cut

sub commit
{
  my ($this) = @_;

  $this->SUPER::commit();

  if(defined $this->err())
  {
    confess("commit() failed: " . DBI::errstr());
  }
}

} # End CBNP::DBI::ErrorHandler::db scope

{ # Begin CBNP::DBI::ErrorHandler::st scope
package CBNP::DBI::ErrorHandler::st;

use strict;
use vars qw($VERSION @ISA);

use Carp;
use DBI;

$VERSION = "0.01";
@ISA = ("DBI::st");

#-------------------------------------------------------------------------------

=head2 execute 

 Usage     : $buffStatement->execute();
             $buffStatement->execute(@bindValues);
 Function  : Executes DBI's execute() on the statement handle.  Will throw an
             exception if it returns abnormally.
 Arguments : Array (optional) - bind values for execution of the statement
 Example   : See Usage
 Returns   : <n/a>

=cut

sub execute
{
  my ($this, @bindValues) = @_;

  my $returnValue = $this->SUPER::execute(@bindValues);
  
  if(defined $this->err())
  {
    confess("execute() failed for SQL \"" .  
            $this->{"Statement"} .  
            "\": " . DBI::errstr());
  }

  return $returnValue;
}

#-------------------------------------------------------------------------------

=head2 fetchrow_arrayref

 Usage     : $rowRef = $buffStatement->fetchrow_arrayref();
 Function  : Executes DBI's fetchrow_arrayref() on the statement handle, will
             throw an exception if the statement returns abnormally. Note that
	     when using fetchrow_arrayref(), once the rows have all been 
	     returned, undef is the next return value, and so a function
             fethrow_arrayref_exists is not possible. 
 Arguments : <n/a> 
 Example   : See Usage
 Returns   : ArrayRef - the row of data requested, or undef if all rows have
                        been returned already 

=cut

sub fetchrow_arrayref
{
  my ($this) = @_;

  # I don't like that this will copy the array an extra time - sorry
  my $answerRef = $this->SUPER::fetchrow_arrayref();

  if(defined $this->err())
  {
    confess("fetchrow_arrayref() failed for SQL \"" .
            $this->{"Statement"} . "\": " .
            DBI::errstr());
  }
  return $answerRef;
}

#-------------------------------------------------------------------------------

=head2 fetchrow_array

 Usage     : @row = $buffStatement->fetchrow_array();
 Function  : Executes DBI's fetchrow_array() on the statement handle, will
             throw an exception if the statement returns abnormally. Note that
	     when using fetchrow_array(), once the rows have all been returned,
	     undef is the next return value, and so a function 
             fetchrow_array_exists is not possible. 
 Arguments : <n/a> 
 Example   : See Usage
 Returns   : Array - the row of data requested, or undef if all rows have
                     been returned already 

=cut

sub fetchrow_array
{
  my ($this) = @_;

  # I don't like that this will copy the array an extra time - sorry
  my @answer = $this->SUPER::fetchrow_array();

  if(defined $this->err())
  {
    confess("fetchrow_array() failed for SQL \"" .
            $this->{"Statement"} . "\": " .
            DBI::errstr());
  }

  return wantarray ? @answer : $answer[0];
}

#-------------------------------------------------------------------------------

=head2 fetchall_arrayref 

 Usage     : $answerListRef = $buffStatement->fetchall_arrayref();
             $answerListRef = $buffStatement->fetchall_arrayref($sliceArrayRef);
             $answerListRef = $buffStatement->fetchall_arrayref($sliceHashRef);
 Function  : Execute's DBI's fetchall_arrayref() on the statement handle. 
             Will throw an exception if it returns abnormally(). 
 Arguments : Array ref (optional) - slice to pass to DBI's fetchall_arrayref()
             Hash ref (optional) - slice to pass to DBI's fetchall_arrayref()
 Example   : See Usage
 Returns   : Array (or hash) ref - the reference returned by fetchall_arrayref() 

=cut

sub fetchall_arrayref
{
  my ($this, $sliceRef) = @_;

  my $answerRef = $this->SUPER::fetchall_arrayref($sliceRef);
  
  if(defined $this->err())
  {
    confess("fetchall_arrayref() failed for SQL \"" .
            $this->{"Statement"} .
            "\": " . DBI::errstr());
  }

  return $answerRef; 
}

#-------------------------------------------------------------------------------

=head2 fetchall_arrayref_exists 

 Usage     : $answerListRef = 
               $buffStatement->fetchall_arrayref_exists();
             $answerListRef = 
               $buffStatement->fetchall_arrayref_exists($sliceArrayRef);
             $answerListRef = 
               $buffStatement->fetchall_arrayref_exists($sliceHashRef);
 Function  : Execute's DBI's fetchall_arrayref() on the statement handle. 
             Will throw an exception if no data is returned, or if it returns
             abnormally.  Use this when you NEED values returned.
 Arguments : Array ref (optional) - slice to pass to DBI's fetchall_arrayref()
             Hash ref (optional) - slice to pass to DBI's fetchall_arrayref()
 Example   : See Usage
 Returns   : Array or Hash ref - the reference returned by fetchall_arrayref() 

=cut

sub fetchall_arrayref_exists
{
  my ($this, $sliceRef) = @_;

  my $answerRef = $this->SUPER::fetchall_arrayref($sliceRef);
 
  if(defined $this->err())
  {
    confess("fetchall_arrayref() failed for SQL \"" .
            $this->{"Statement"} .
            "\": " . DBI::errstr());
  }

  if(!defined $answerRef ||
     !defined $answerRef->[0] || 
     !defined $answerRef->[0]->[0])
  {
    confess("fetchall_arrayref_exists() resulted in no data for SQL \"" .
            $this->{"Statement"} . "\"");
  }

  return $answerRef; 
}

} # End CBNP::DBI::ErrorHandler::st scope

1; # Got one?

__END__

=head1 AUTHOR

Clinton Torres (clinton.torres@llnl.gov)

=head1 SEE ALSO

=cut