package LEOCHARRE::Database;
use DBI;
use strict;
use warnings;
use Carp;
use LEOCHARRE::DEBUG;
#use LEOCHARRE::Database::Base ':all'; # for functional
use base 'LEOCHARRE::Database::Base';

our $VERSION = sprintf "%d.%02d", q$Revision: 1.12 $ =~ /(\d+)/g;




=pod

=head1 NAME

LEOCHARRE::Database - common database methods for oo

=head1 SYNOPSIS

Mymod.pm:

   package Mymod;
   use base 'LEOCHARRE::Database';

   1;


script.pl:

   use Mymod;

   my $m = new Mymod({ 
      DBHOST => 'localhost' , 
      DBUSER => 'username',
      DBPASSWORD => 'passwerd',
      DBNAME => 'superdb',
   });

   $m->dbh;

   my $a = $m->dbh_sth('select * from avocado where provider = ?');
   $a->execute(3);

   $m->dbh_sth('INSERT INTO fruit (name,provider) values(?,?)')->execute('grape','joe'); # gets prepared and cached
   
   $m->dbh_sth('INSERT INTO fruit (name,provider) values(?,?)')->execute('orange','joe'); # uses the previous cached prepared handle


=head1 DESCRIPTION

This is meant tobe used as base by another oo module.

This can be passed an open database handle via DBH or it will attempt to open a new connection.
By default it attempts to open a mysql connection.
It can also open a sqlite connection.

Basically, if you provide the argument DBABSPATH, will attempt to open a mysql connection instead.

PLEASE NOTE autocommit is off. DESTROY calls commit and finishes and closes handles.

=head2 To open a mysql connection

   new ({ 
      DBHOST => $host,
      DBNAME => $dbname,
      DBUSER => $dbuser,
      DBPASSWORD => $pw,
   });

=head2 To open a sqlite connection

   new ({ 
      DBABSPATH => '/home/myself/great.db',
   });

=head2 To use existing connection

   new ({ 
      DBH => $dbh,
   });


=head1 METHODS

These are meant to be inherited by your module with 

   use base 'LEOCHARRE::Database';

=cut


sub dbh {
   my $self = shift;
   unless( $self->{DBH} ){
      
      $self->{AUTOCOMMIT} ||=0; 

      if( ! defined $self->{DBABSPATH} ){
         debug('will open mysql connection..'); 
   	   my $dbname		= $self->{DBNAME} or confess('missing DBNAME argument to constructor');
		   my $user			= $self->{DBUSER} or confess('missing DBUSER argument to constructor');
		   my $password	= $self->{DBPASSWORD} or confess('missing DBPASSWORD argument to constructor');

	   	$self->{DBH} = DBI::connect_mysql($self->{DBNAME}, $self->{DBUSER}, $self->{DBPASSWORD}, $self->{DBHOST})
            or die('connect_mysql failed');   
         debug("ok");
      }

      
      else {
         $self->{DBABSPATH} or confess('DBABSPATH argument to constructor bad');    
         debug("will open sqlite connection..$$self{DBABSPATH} ");         
         
         $self->{DBH} = DBI::connect_sqlite($self->{DBABSPATH}) or die('conect_sqlite failed');         
      }
      $self->{__DBH_IS_LOCAL_} = 1;
      
      $self->{DBH}->{RaiseError} = 1; # freak out when things go bad
      #$self->{DBH}->{PrintError} = 1; # print error or not
      
      $self->{DBH}->{AutoCommit} = $self->{AUTOCOMMIT};

      if ($DEBUG){
         $LEOCHARRE::Database::Base::DEBUG=1;
      }
         
      
   }
   return $self->{DBH};
}

=head3 dbh()

returns database handle

=cut

# did the connection happen here?
sub _dbh_is_local {
   my $self = shift;
   $self->{__DBH_IS_LOCAL_} ||=0;
   return $self->{__DBH_IS_LOCAL_};
}


sub dbh_selectcol {
   my ($self, $statement) = @_; 
   return $self->dbh->selectcol($statement); 
}

=head2 dbh_selectcol()

argument is statement
will select and return array ref

   my $users = $self->dbh_selectcol("SELECT user FROM users WHERE type = 'm'");

Now users has ['joe','rita','carl']

This is useful sometimes.

=cut

sub dbh_do {
   my($self, $arg) = @_;
   defined $arg or confess('missing arg');
   ref $arg eq 'HASH' or confess('arg must be hashref');
   defined $arg->{sqlite} or confess('missing sqlite key');
   defined $arg->{mysql} or confess('missing mysql key');

   my $q = $self->dbh->is_mysql ? $arg->{mysql} : $arg->{sqlite};

   $self->dbh->do($q) ; # TODO or .. ????

   return 1;   
}

=head2 dbh_do()

argis hash ref with mysql and sqlite keys

will select one or the other with dbh_is_mysql etc

   $self->dbh_do({
      sqlite => 'CREATE TABLE bla( name varchar(25) )',
      mysql  => 'CREATE TABLE bla( name varchar(25) )',  
   });

=cut

sub dbh_count {
   my ($self,$statement) = @_;
   return $self->dbh->rows_count($statement);   
}

=head3 dbh_count()

argument is statement
returns count number
you MUST have a COUNT(*) in the select statement

   my $matches = $self->dbh_count('select count(*) from files');

=cut


sub dbh_sth {   
   my ($self, $statement) = @_;
   $statement or confess("missing statement argument");
   
   unless ($self->{_handles}->{$statement}){
      $self->{_handles} ||={};
      debug("preparing [$statement]..");
      
      local $self->dbh->{RaiseError};
      my $sth = $self->dbh->prepare($statement);
      defined $sth or confess("statment [$statement] failed to prepare, ". $self->dbh->errstr );      
      $self->{_handles}->{$statement} = $sth; 
   }

   return $self->{_handles}->{$statement};  
}


=head2 dbh_sth()

argument is a statment, returns handle
it will cache in the object, subsequent calls are not re-prepared

   my $delete = $self->dbh_sth('DELETE FROM files WHERE id = ?');
   $delete->execute(4);
   
   # or..
   for (@ids){
      $self->dbh_sth('DELETE FROM files WHERE id = ?')->execute($_);
   } 

If the prepare fails, confess is called.

=cut


sub dbh_is_sqlite {
	my $self = shift;
   return $self->dbh->is_sqlite;
}

sub dbh_is_mysql {
	my $self = shift;
   return $self->dbh->is_mysql;
}

sub dbh_driver {
	my $self = shift;
   return $self->dbh->driver;
}

=head3 dbh_is_mysql()

returns boolean

=head3 dbh_is_sqlite()

returns boolean

=head3 dbh_driver()

returns name of DBI Driver, sqlite, mysql, etc.
Currently mysql is used, sqlite is used for testing. For testing the package, you don't need to have
mysqld running.

=cut


sub dbh_droptable {
   my ($self,$tablename) = @_;
   return $self->dbh->drop_table($tablename);
}

sub dbh_table_exists {
   my ($self,$tablename) = @_;
   return $self->dbh->table_exists($tablename);
}

sub dbh_table_dump {
   my ($self,$tablename) = @_;
   return $self->dbh->table_dump($tablename);
}




=head2 dbh_table_exists()

argument is table name, returns boolean

=head2 dbh_table_dump()

argument is table name
returns string of dump of table suitable for print to STDERR

=head2 dbh_droptable()

arg is table name, drops the table.
returns boolean
will drop IF EXISTS

=cut

sub dbh_lid {
   my ($self,$tablename) = @_;
   return $self->dbh->lid($tablename);
}

=head2 dbh_lid()

arg is table name, returns last insert id. 
returns undef if not there

=cut


# for retro
sub dbh_close_active_handles {
   my $self = shift;
   defined $self->dbh or return 1;
   return $self->dbh->close_active_handles;   
}



sub DESTROY {
   my $self = shift;  

   defined $self->dbh or return 1;
   
   $self->_dbh_is_local or debug("dbh is not local\n") and return 1;
#   unless( $self->dbh->{AutoCommit} or return 1;
   debug("dbh is local..");
   

   $self->dbh->close_active_handles;

   
     
   unless ( $self->dbh->{AutoCommit} ){
      debug("committing.."); 
      local $self->dbh->{RaiseError};      
      $self->dbh->commit or confess("cannot commit.. ".$self->dbh->errstr);
      debug("done.\n");
   }

   {      
      # sqlite reports errors here.. a bug
      # "closing dbh with active statement handles at.."
      # this is a brute force approach to get rid of that warning
      no warnings;
      (local $SIG{'__WARN__'} = sub {} ) if $self->dbh->is_sqlite;      
      
      $self->dbh->disconnect;
      debug("disconnected.\n");
   };   
  
   return 1;
}

=head2 dbh_close_active_handles()

closes ChildHandles that are active, finishes and undefines them.
returns true + number of active handles were finished and undefined here

=head2 DESTROY()

If the database handle was created in this object and not passed via constructor, then we
close all handles, commit, and disconnect.

finishes active database handles etc, makes a commit to the database.
Note, this method is called automatically.



=head1 CAVEATS

=head2 IMPORTANT NOTE AUTOCOMMIT

Autocommit is set to 0 by default.
That means you should commit after indexing_lock(), indexing_lock_release(), delete_record()

DESTROY will finish and commit if there are open handles created by the object

=head1 DEBUG

To turn on debug, 

   $LEOCHARRE::Database::DEBUG = 1;

=head1 SEE ALSO

L<LEOCHARRE::CLI>
L<LEOCHARRE::Dev>
L<DBI>

=head1 AUTHOR

Leo Charre leocharre at cpan dot org

=head1 COPYRIGHT

Copyright (c) 2007 Leo Charre. All rights reserved.

=head1 LICENSE

This package is free software; you can redistribute it and/or modify it under the same terms as Perl itself, i.e., under the terms of the "Artistic License" or the "GNU General Public License".

=head1 DISCLAIMER

This package is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

See the "GNU General Public License" for more details.

=cut


1;
