package Basset::DB;

#Basset::DB 2002, 2003, 2004 James A Thomason III
#Basset::DB is distributed under the terms of the Perl Artistic License.

$VERSION = '1.01';

=pod

=head1 NAME

Basset::DB - talks to your database and gives you a few helper database methods.

=head1 AUTHOR

Jim Thomason, jim@jimandkoka.com

=head1 SYNOPSIS

 #buried in the bowels of a module somewhere
 my $driver = Basset::DB->new();
 my $stmt = $driver->prepare('select * from some_table');

=head1 DESCRIPTION

You have a database. You're using Basset::Object::Persistent. You need to store objects. You need
to talk to your database. You're using Basset::DB::Table for all of your table related stuff. But,
some things are just simply database related (like connecting, transactions, etc.) for that, you
need something higher. Basset::DB does just that.

=cut

use Basset::Object;
use DBI 1.32 qw(:sql_types);

@ISA = qw(Basset::Object);

use strict;
use warnings;

=pod

=head1 ATTRIBUTES

=cut

#read only attribute. Hands you back the internal DBI handle.

__PACKAGE__->add_attr('handle');

=pod

=begin btest(handle)

my $o = __PACKAGE__->new();
$test->ok($o, "Got object");
$test->is(scalar(__PACKAGE__->handle), undef, "could not call object method as class method");
$test->is(__PACKAGE__->errcode, "BO-08", "proper error code");
$test->is($o->handle('abc'), 'abc', 'set handle to abc');
$test->is($o->handle(), 'abc', 'read value of handle - abc');
my $h = {};
$test->ok($h, 'got hashref');
$test->is($o->handle($h), $h, 'set handle to hashref');
$test->is($o->handle(), $h, 'read value of handle  - hashref');
my $a = [];
$test->ok($a, 'got arrayref');
$test->is($o->handle($a), $a, 'set handle to arrayref');
$test->is($o->handle(), $a, 'read value of handle  - arrayref');

my $poolkey = join(',', map{defined $_ ? $_ : 'undef'} ($o->dsn, $o->user, $o->pass));
delete $o->pool->{$poolkey};

=end btest(handle)

=cut


__PACKAGE__->add_attr('dsn');

=pod

=begin btest(dsn)

my $o = __PACKAGE__->new();
my $dsn = $o->dsn;
$test->ok($o, "Got object");
$test->is(scalar(__PACKAGE__->dsn), undef, "could not call object method as class method");
$test->is(__PACKAGE__->errcode, "BO-08", "proper error code");
$test->is($o->dsn('abc'), 'abc', 'set dsn to abc');
$test->is($o->dsn(), 'abc', 'read value of dsn - abc');
my $h = {};
$test->ok($h, 'got hashref');
$test->is($o->dsn($h), $h, 'set dsn to hashref');
$test->is($o->dsn(), $h, 'read value of dsn  - hashref');
my $a = [];
$test->ok($a, 'got arrayref');
$test->is($o->dsn($a), $a, 'set dsn to arrayref');
$test->is($o->dsn(), $a, 'read value of dsn  - arrayref');
$test->is($o->dsn($dsn), $dsn, "reset dsn");

my $poolkey = join(',', map{defined $_ ? $_ : 'undef'} ($o->dsn, $o->user, $o->pass));
delete $o->pool->{$poolkey};

=end btest(dsn)

=cut

__PACKAGE__->add_attr('user');

=pod

=begin btest(user)

my $o = __PACKAGE__->new();
my $user = $o->user;
$test->ok($o, "Got object");
$test->is(scalar(__PACKAGE__->user), undef, "could not call object method as class method");
$test->is(__PACKAGE__->errcode, "BO-08", "proper error code");
$test->is($o->user('abc'), 'abc', 'set user to abc');
$test->is($o->user(), 'abc', 'read value of user - abc');
my $h = {};
$test->ok($h, 'got hashref');
$test->is($o->user($h), $h, 'set user to hashref');
$test->is($o->user(), $h, 'read value of user  - hashref');
my $a = [];
$test->ok($a, 'got arrayref');
$test->is($o->user($a), $a, 'set user to arrayref');
$test->is($o->user(), $a, 'read value of user  - arrayref');
$test->is($o->user($user), $user, "reset user");

my $poolkey = join(',', map{defined $_ ? $_ : 'undef'} ($o->dsn, $o->user, $o->pass));
delete $o->pool->{$poolkey};

=end btest(user)

=cut

__PACKAGE__->add_attr('pass');

=pod

=begin btest(pass)

my $o = __PACKAGE__->new();
my $pass = $o->pass();
$test->ok($o, "Got object");
$test->is(scalar(__PACKAGE__->pass), undef, "could not call object method as class method");
$test->is(__PACKAGE__->errcode, "BO-08", "proper error code");
$test->is(scalar($o->pass), undef, 'pass is undefined');
$test->is($o->pass('abc'), 'abc', 'set pass to abc');
$test->is($o->pass(), 'abc', 'read value of pass - abc');
my $h = {};
$test->ok($h, 'got hashref');
$test->is($o->pass($h), $h, 'set pass to hashref');
$test->is($o->pass(), $h, 'read value of pass  - hashref');
my $a = [];
$test->ok($a, 'got arrayref');
$test->is($o->pass($a), $a, 'set pass to arrayref');
$test->is($o->pass(), $a, 'read value of pass  - arrayref');
$test->is($o->pass($pass), $pass, "reset pass");

my $poolkey = join(',', map{defined $_ ? $_ : 'undef'} ($o->dsn, $o->user, $o->pass));
delete $o->pool->{$poolkey};

=end btest(pass)

=cut


=pod

=over

=item failed

Boolean attribute, set internally if the current transaction has been failed.

=cut

__PACKAGE__->add_attr('failed');

=pod

=begin btest(failed)

my $o = __PACKAGE__->new();
$test->ok($o, "Got object");
$test->is(scalar(__PACKAGE__->failed), undef, "could not call object method as class method");
$test->is(__PACKAGE__->errcode, "BO-08", "proper error code");
$test->is(scalar($o->failed), undef, 'failed is undefined');
$test->is($o->failed('abc'), 'abc', 'set failed to abc');
$test->is($o->failed(), 'abc', 'read value of failed - abc');
my $h = {};
$test->ok($h, 'got hashref');
$test->is($o->failed($h), $h, 'set failed to hashref');
$test->is($o->failed(), $h, 'read value of failed  - hashref');
my $a = [];
$test->ok($a, 'got arrayref');
$test->is($o->failed($a), $a, 'set failed to arrayref');
$test->is($o->failed(), $a, 'read value of failed  - arrayref');

my $poolkey = join(',', map{defined $_ ? $_ : 'undef'} ($o->dsn, $o->user, $o->pass));
delete $o->pool->{$poolkey};

=end btest(failed)

=back

=cut




=pod

=head1 METHODS

=cut

sub init {

	my $self = shift->SUPER::init(
		'stack' => 0,
		@_
	) or return;

	my $poolkey = join(',', map{defined $_ ? $_ : 'undef'} ($self->pkg, $self->dsn, $self->user, $self->pass));

	if (my $pooledobj = $self->pool->{$poolkey}) {
		$self = $pooledobj;
	}

	my $h = $self->create_handle(
		'dsn'			=> $self->dsn,
		'user'			=> $self->user,
		'pass'			=> $self->pass,
		'AutoCommit'	=> 0,
		#'transactions' => $self->transactions,
	) or return;
	
	$self->handle($h) or return;

	$self->pool->{$poolkey} = $self;

	return $self;
}

__PACKAGE__->add_class_attr('pool', {});

=pod

=begin btest(new__only)

$test->ok(1, "Due to pooling, SUPER new tests cannot work. Assumes success");

=end btest(new__only)

=pod

=begin btest(init)

my $o = __PACKAGE__->new();
$test->ok($o, "got object for init");
$test->ok($o->dsn, "DSN is defined");
$test->ok($o->user, "user is defined");

local $@ = undef;

my $o2 = __PACKAGE__->new();
$test->ok($o, "got second object for init");
$test->is($o, $o2, "objects match, due to pooling");
$test->is($o->handle, $o2->handle, "handles match, due to pooling");


=end btest(init)

=cut


=pod

=over

=item create_handle

Takes a hash of values (dsh, user, pass) which are used to create a new database handel.
By default, uses DBI's connect_cached method. Can be overridden in subclasses.

=cut

sub create_handle {
	my $class = shift;
	my %init = @_;

	my $h = DBI->connect_cached(
		$init{'dsn'},
		$init{'user'},
		$init{'pass'},
		{'AutoCommit' => $init{'AutoCommit'}},
	) or return $class->error(DBI->errstr, "BD-01");

	return $h;
}

=pod

=begin btest(create_handle)

local $@ = undef;
eval {
	__PACKAGE__->create_handle(); #fails w/o args
};
$test->ok($@, "DBI connect failed");

=end btest(create_handle)

=cut


sub DESTROY {
	my $self = shift;
	if ($self->handle && $self->stack) {
		$self->notify("warnings", "Warning - driver destroyed with transaction stack");
		$self->stack(0);
		$self->handle->rollback;
	};
};

=pod

=begin btest(DESTROY)

=end btest(DESTROY)

=cut


=pod

=item AUTOLOAD

friggin' DBI cannot be subclassed. So AUTOLOAD sits in between. Any method called on a Basset::DB
object that it doesn't understand creates a new method that passes through to the internal handle
and calls the method on that. So, obviously, only use DBI methods.

=cut

sub AUTOLOAD {
	my $self = shift;
	(my $method = $Basset::DB::AUTOLOAD) =~ s/^(.+):://;
	
	if ($method ne 'DESTROY') {

		if (defined $self->handle){ 
			no strict 'refs';
			my $pkg = $self->pkg;
			
			*{$pkg . "::$method"}  = sub {
				my $self	= shift;

				if (my $handle = $self->handle) {
					local $@ = undef;
					my $rc = undef;
					eval {
						$rc = $handle->$method(@_);
					};
					if ($@) {
						return $self->error("Cannot call method ($method) : DBI does not support ($@)", "BD-14");
					} else {
						return $rc || $self->error($handle->errstr, "BD-11");
					};
				} else {
					return $self->error("Cannot call method ($method) : no handle", "BD-12");
				}
			};

			return $self->$method(@_);
#			shift->$accessor($method, @static_args, @_)};
#			return $self->handle->$method(@_);
		} else {
			return $self->error("Cannot do anything without handle", "BD-09");
		};
	}

};

=pod

=begin btest(AUTOLOAD)

=end btest(AUTOLOAD)

=cut


__PACKAGE__->add_attr('stack');

{
	my $stacks = {};

=pod

=item stack

This is your transaction stack for your driver. You will rarely (if ever) need to see
this directly.

 $driver->begin();
 print $driver->stack(); #1
 $driver->begin();
 print $driver->stack(); #2
 $driver->begin();
 print $driver->stack(); #3
 
=cut
	
=pod

=begin btest(stack)

my $o = __PACKAGE__->new();
$test->ok($o, "got object");
$test->is($o->stack, 0, "stack is 0");
$test->is($o->stack(5), 5, "stack does increment w/transatcions");
$test->is($o->stack, 5, "stack is at 5");

my $poolkey = join(',', map{defined $_ ? $_ : 'undef'} ($o->dsn, $o->user, $o->pass));
delete $o->pool->{$poolkey};

=end btest(stack)

=cut


=pod

=item begin

Adds 1 to your transaction stack

=cut

	sub begin {
		my $self = shift;
		
		return $self->error("Cannot begin transaction - failed", "BD-13") if $self->failed;
		
		return $self->stack($self->stack + 1);
	}

=pod

=begin btest(begin)

my $o = __PACKAGE__->new();
$o->wipe;
$test->ok($o, "got transaction enabled object");
$test->is($o->stack, 0, "stack is 0");
$test->is($o->begin, 1, "began transaction, stack is 1");
$test->is($o->stack, 1, "stack is 1");
$test->is($o->begin, 2, "began transaction, stack is 2");
$test->is($o->stack, 2, "stack is 2");

my $poolkey = join(',', map{defined $_ ? $_ : 'undef'} ($o->dsn, $o->user, $o->pass));
delete $o->pool->{$poolkey};

=end btest(begin)

=cut


=pod

=item end

Subtracts 1 from your transaction stack.

=cut

	sub end {
		my $self = shift;
		
		my $stack = $self->stack($self->stack - 1);

		if ($stack <= 0) {

			$self->stack(0);

			return $self->error("Cannot end transaction - failed", "BD-13") if $self->failed;

			$self->finish() or return;

			return '0 but true';
		} else {
			return $stack;
		};
	}

=pod

=begin btest(end)

my $o = __PACKAGE__->new();
$o->wipe;
$test->ok($o, "got transaction enabled object");
$test->is($o->stack, 0, "stack is 0");
$test->is($o->begin, 1, "began transaction, stack is 1");
$test->is($o->stack, 1, "stack is 1");
$test->is($o->begin, 2, "began transaction, stack is 2");
$test->is($o->stack, 2, "stack is 2");
$test->is($o->end, 1, "end transaction, stack is 1");
$test->is($o->stack, 1, "stack is 1");
$test->is($o->end, '0 but true', "end transaction, stack is 0 (but true)");
$test->is($o->stack, 0, "stack is 0");

my $poolkey = join(',', map{defined $_ ? $_ : 'undef'} ($o->dsn, $o->user, $o->pass));
delete $o->pool->{$poolkey};

=end btest(end)

=cut


=pod

=item finish

automagically finishes your transaction and sets your stack back to 0, regardless of how many items are on your stack.
Use this method with extreme care.

=cut

	sub finish {
		my $self = shift;

		my $handle = $self->handle;

		$handle->commit()
			or return $self->error($handle->errstr, "BD-07");

		$self->stack(0);
		return 1;
	}
	
=pod

=begin btest(finish)

my $o = __PACKAGE__->new();
$o->wipe;
$test->ok($o, "got transaction enabled object");
$test->is($o->stack, 0, "stack is 0");
$test->is($o->begin, 1, "began transaction, stack is 1");
$test->is($o->stack, 1, "stack is 1");
$test->is($o->begin, 2, "began transaction, stack is 2");
$test->is($o->stack, 2, "stack is 2");
$test->is($o->finish, 1, "finished transaction");
$test->is($o->stack, 0, "stack is 0");

my $poolkey = join(',', map{defined $_ ? $_ : 'undef'} ($o->dsn, $o->user, $o->pass));
delete $o->pool->{$poolkey};

=end btest(finish)

=cut



=pod

=item fail

fails your transaction and rolls it back from the database. If you just want to fail your transaction
but otherwise not roll it back, then simply set failed = 1.

=cut

	sub fail {
		my $self = shift;
		
		return $self->wipe();
	}
	
=pod

=begin btest(fail)

my $o = __PACKAGE__->new();
$o->wipe;
$test->ok($o, "got transaction enabled object");
$test->is($o->stack, 0, "stack is 0");
$test->is($o->begin, 1, "began transaction, stack is 1");
$test->is($o->stack, 1, "stack is 1");
$test->is($o->begin, 2, "began transaction, stack is 2");
$test->is($o->stack, 2, "stack is 2");
$test->is($o->fail, 1, "failed transaction");
$test->is($o->stack, 0, "stack is 0");

my $poolkey = join(',', map{defined $_ ? $_ : 'undef'} ($o->dsn, $o->user, $o->pass));
delete $o->pool->{$poolkey};

=end btest(fail)

=cut

=pod

=item unfail

unfails a transaction. If a fatal error occurs and you want to continue, you must unfail

=cut

sub unfail {
	my $self = shift;
	$self->failed(0);
	$self->handle->rollback();
	return '0 but true';
}

=pod

=begin btest(unfail)

my $o = __PACKAGE__->new();
$o->wipe;
$test->ok($o, "got transaction enabled object");
$test->is($o->failed(1), 1, "driver transaction failed");
$test->ok($o->unfail, "unfailed transaction");
$test->ok(! $o->failed, "transaction no longer failed");

my $poolkey = join(',', map{defined $_ ? $_ : 'undef'} ($o->dsn, $o->user, $o->pass));
delete $o->pool->{$poolkey};

=end btest(unfail)

=cut

=pod

=item wipe

fails your transaction and rolls it back from the database if you have pending items on your stack.

=cut

	sub wipe {
		my $self = shift;

		my $handle = $self->handle;
		$handle->rollback()
			or return $self->error($handle->errstr, "BD-08");
	
		$self->stack(0);
		$self->failed(0);
	
		return 1;
	};
};

=pod

=begin btest(wipe)

my $o = __PACKAGE__->new();
$o->wipe;
$test->ok($o, "got transaction enabled object");
$test->is($o->stack, 0, "stack is 0");
$test->is($o->begin, 1, "began transaction, stack is 1");
$test->is($o->stack, 1, "stack is 1");
$test->is($o->begin, 2, "began transaction, stack is 2");
$test->is($o->stack, 2, "stack is 2");
$test->is($o->wipe, 1, "wiped transaction");
$test->is($o->stack, 0, "stack is 0");

my $poolkey = join(',', map{defined $_ ? $_ : 'undef'} ($o->dsn, $o->user, $o->pass));
delete $o->pool->{$poolkey};

=end btest(wipe)

=cut

=pod

=item copy

Copying Basset::DB objects is frowned upon. Nonetheless, if you must do it, you're still going
to get the same database handle back. That is to say, the exact same object.

Note - as a result of how this has to work (and some DBI bitching), copying Basset::DB objects
is not thread safe.

=cut

sub copy {
	my $self = shift;
	if (@_) {
		return $self->SUPER::copy(@_);
	} else {
		#grab our handle
		my $h = $self->handle;
		#wipe it out. Our copy is primitive and just dumps and evals the object
		$self->handle(undef);
		my $copy = $self->SUPER::copy;
		#reset the handle
		$self->handle($h);
		#set it in the copy
		$copy->handle($h);
		
		return $copy;
	}
}

=pod

=begin btest(copy)

my $obj = __PACKAGE__->new();
#$test->ok($obj, "Got object for copy test");
my $o2 = $obj->copy;
#$test->is($obj->copy, $o2->copy, "Copied objects match");

=end btest(copy)

=cut



=pod

=item sql_type

This is a wrapper method to DBI's sql_types constants. Pass in a string value consisting of the
sql type string, and it spits back the relevant DBI constant.

 my $some_constant = Basset::DB->sql_type('SQL_INTEGER');

Very useful if you're binding values or such.

=cut

our %cache = ();
sub sql_type {
	my $class	= shift;
	
	return $class->error("Cannot return type without type", "BD-02") unless @_;
	
	my $type	= shift;

	return undef unless defined $type;

	#return $type if $type =~ /^\d+$/;

	my $return	= $cache{$type} || eval $type || undef;

	$cache{$type} = $return;

	return $return;
};

=pod

=begin btest(sql_type)

$test->is(scalar(__PACKAGE__->sql_type), undef, "Cannot return type w/o type");
$test->is(__PACKAGE__->errcode, "BD-02", "proper error code");
{
	use DBI qw(:sql_types);
	$test->is(__PACKAGE__->sql_type('SQL_INTEGER'), SQL_INTEGER(), "proper type for integer");
	$test->is(__PACKAGE__->sql_type('SQL_INTEGER'), SQL_INTEGER(), "proper type for integer");
	$test->is(__PACKAGE__->sql_type('SQL_VARCHAR'), SQL_VARCHAR(), "proper type for varchar");
	$test->is(__PACKAGE__->sql_type(SQL_VARCHAR()), SQL_VARCHAR(), "proper type for varchar, given int");
	$test->is(__PACKAGE__->sql_type('__j_junk_type'), undef, "unknown type returns undef");
	$test->is(__PACKAGE__->sql_type(undef), undef, "undef type returns undef");
}

=end btest(sql_type)

=cut


=pod

=item tables

returns an array of all tables in your database. You may optionally pass in a database handle
to get all of the tables for that handle instead of the default

=cut

sub tables {
	my $class = shift;

	my $driver = shift || $class->new();

	my $query = "show tables";

	my $stmt = $driver->prepare_cached($query)
		or return $class->error($driver->errstr, "BD-03");

	$stmt->execute() or return $class->error($stmt->errstr, "BD-04");

	my @tables = ();
	while (my ($table) = $stmt->fetchrow_array){
		push @tables, $table;
	};

	$stmt->finish()
		or return $class->error($stmt->errstr, "BD-05");

	return @tables;

};

=pod

=item ping

just a wrapper around DBI's ping

=cut

sub ping {
	return shift->handle->ping;
};

=pod

=begin btest(tables)

=end btest(tables)

=cut


=pod

=item optimize_tables

MySQL only, most likely. Calls the "optimize table" command on all tables in your database,
or only upon those tables that you've passed in, if you prefer.

=cut

sub optimize_tables {
	my $class = shift;
	my @tables = @_ || $class->tables;

	my $driver = $class->new();

	foreach my $table (@tables){
		my $query = "optimize table $table";

		my $stmt = $driver->prepare_cached($query)
			or return $class->error($driver->errstr, "BD-03");

		$stmt->execute() or return $class->error($stmt->errstr, "BD-04");

		$stmt->finish()
			or return $class->error($stmt->errstr, "BD-05");
	};

	return @tables;
};

=pod

=begin btest(optimize_tables)

=end btest(optimize_tables)

=cut


1;
