The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.
package SQL::SqlObject;

#use 5.006;
use strict;
use warnings;
use Carp;
use DBI;
use SQL::SqlObject::Config;

# 110803 - VERSION incremented to 0.02 for first CPAN release 
# 101903 - fixed argument verification for insert_select
#        - added connection arg cacheing
#        - we now cache db_name, db_dsn and db_pre if they
#          are provided as arguments to connect_string() -cz
# 011003 - arguments for constructor now come from
#          SQL::SqlObject::Config -cz
# 010803 - added constuctor_args/process_contructror args -cz
# 010603 - added AUTOLOAD to pass through method calls -cz
# 052699 - Minor fix to Hashes Sub -cz

our $VERSION     = '0.02';
our %SqlConfig;  # read from SQL::SqlObject::Config

sub new
{

    # install confguration, if necessary
    unless (%SqlConfig)
    {
	no strict 'refs';
	*SQL::SqlObject::SqlConfig = \%SQL::SqlObject::Config::SqlConfig;
    }

    my ($class) = @_;
    my $self;


    # we don't really support clones...
    if (ref $class)
    {
	# clones must have us somwhere in their ancestory
	unless (UNIVERSAL::isa($class, __PACKAGE__))
	{
		Carp::confess (
			       "process_constructor_args: attemp to clone",
			       " from unrelated class: ", ref $class
			      );
	}

	($class) = $class =~ /(.*?)=/;
    }

    $self = bless {}, $class;
    $self->process_constructor_args(@_);
    # post construction initialization
    $self->_init if $self->can('_init');

    return $self;
}

sub clone { shift->new(@_) }

sub DESTROY { $_[0]->disconnect }

sub process_constructor_args
{

    unless (@_>1) {
	Carp::confess "process_constructor_args: wrong number of parameters"
    }

    my $self  = shift;
    my $class = shift;

    # coerse input to hash reference
    my $args = {};
    if ($_ = ref $_[0])
    {
	if (/HASH/)
	{
	    while (my ($k, $v) = each %{ $_[0] })
	    {
	        my $arg_ix = SQL::SqlObject::Config::arg_index($k);
	        $k =~ y/A-Z-/a-z/d;
		$args->{$SqlConfig{ARGS}[ $arg_ix ][0]} = $v;
	    }
	}

	elsif (/ARRAY/)
	{
	    $args = {
		     map { $SqlConfig{ARGS}[$_][0] => $_[0][$_] } 0..$#{$_[0]}
		    };
	}

	else
	{
	    Carp::Confess (q(process_constructor_args encountered ),
			   q(unexpected reference type ), $_);
	}
    }

    elsif (@_)
    {
	if ($_[0] =~ /^-/)
	{
	    # input aleardy in key-value format
	    if (@_ % 2)
	    {
		Carp::confess ('process_constructor_args uneven number ',
			       'of input parameters')
	    }

	    for (my $input_ix = 0; $input_ix < @_; $input_ix += 2 )
	    {
		my $arg_ix = SQL::SqlObject::Config::arg_index($_[$input_ix] );
		$args->{ $SqlConfig{ARGS}[ $arg_ix ][0] } = $_[ $input_ix + 1 ];
	    }
	}

	else
	{
	    # input in ordered list format
	    for (0..$#_)
	    {
		$args->{ $SqlConfig{ARGS}[$_][0] } = $_[$_];
	    }
	}
    }

    # now try to supply values for all args
    ARG: for my $arg_id ( 0..$#{ $SqlConfig{ARGS} })
    {
	my $name = $SqlConfig{ARGS}[$arg_id][0];

	# see value if value was supplied as an argument
	if (exists $args->{$name})
        {
	    $self->$name( $args->{$name} );
	    next ARG;
	}

	# if we're cloning look at the parent object first
	if (ref $class and defined $class->$name)
	{
	    $self->$name($class->$name);
	    next ARG;
	}

	# search env variables if any we're supplied
	if ($SqlConfig{ARGS}[$arg_id][2])
	{
	    my $env = $SqlConfig{ARGS}[$arg_id][2];

	    if (ref $env)
	    {
		ENV: for (@$env)
		{
		    next ENV unless exists $ENV{$_};
		    $self->$name( $ENV{$_} );
		    next ARG;
		}
	    }

	    else
	    {
		if (exists $ENV{$env})
		{
		    $self->$name( $ENV{$env} );
		    next ARG;
		}
	    }
	}

	# use default if one was provided
	if ($SqlConfig{ARGS}[$arg_id][3] and
	    $_ = $SqlConfig{ $SqlConfig{ARGS}[$arg_id][3] })
	{
	    $self->$name( $_ );
	    next ARG;
	}
    }

    return $self;
}

# is_connected: toggled on  by dbh
#                       off by disconnect
sub is_connected : lvalue { $_[0]->{connected_P} }

# primary flock of accessors
sub db_name        : lvalue { delete $_[0]->{__connection_args};
                              $#_ and $_[0]->{name}   = $_[1]; $_[0]->{name}}
sub db_name_prefix : lvalue { delete $_[0]->{__connection_args};
                              $#_ and $_[0]->{pre}    = $_[1]; $_[0]->{pre}}
sub db_dsn         : lvalue { delete $_[0]->{__connection_args};
                              $#_ and $_[0]->{dsn}    = $_[1]; $_[0]->{dsn}}
sub db_user        : lvalue { delete $_[0]->{__connection_args};
                              $#_ and $_[0]->{user}   = $_[1]; $_[0]->{user}}
sub db_password    : lvalue { delete $_[0]->{__connection_args};
                              $#_ and $_[0]->{passwd} = $_[1]; $_[0]->{passwd}}
sub Error          : lvalue { $#_ and $DBI::errstr    = $_[1]; $DBI::errstr}
sub dbh            : lvalue
{
  unless ($_[0]->is_connected) {
    $_[0]->{dbh} = $_[0]->connect;
    $_[0]->is_connected = 1;
  }

  $_[0]->{dbh}
}

sub connect
{

    my $self = shift;

    if (@_)
      # cache the arguments, in case we have to reconnect
      # as seems to be needed for insert_select
      {
	$self->{__connection_args} = [ @_ ];
      }

    elsif ($self->{__connection_args})
      # load the connection args from the cache
      {
	@_ = @{ $self->{__connection_args} }
      }

    my $connect_string = $self->connect_string(@_);

    $self->is_connected and $self->disconnect;

    my $dbh  = DBI->connect($connect_string,$self->db_user,$self->db_password);

    my $err = $self->Error;
    $err and Carp::confess (
			    q(Whoops - couldn\'t connect to ),
			    $_[0] || $self->db_name, "\n",
			    'Error:', $err
			   );

    return $dbh;
}

# build the connect string
sub connect_string
{
    my ($self, $name, $dsn, $pre) = @_;

    $self->db_name        ||= $name if $name;
    $self->db_dsn         ||= $dsn  if $dsn;
    $self->db_name_prefix ||= $pre  if $pre;

    defined $name or $name = $self->db_name;
    defined $dsn  or $dsn  = $self->db_dsn
      or Carp::confess "No data source named for SqlObject connect string";
    defined $pre  or $pre  = $self->db_name_prefix;

    my $other = '';
    if ($SqlConfig{OTHER_ARGS})
    {
        if (ref $SqlConfig{OTHER_ARGS})
	{
	    for (@{$SqlConfig{OTHER_ARGS}})
	    {
	      my $val = $self->$_ or next;
	      my ($key) = (/^(?:.*?_)?(.*)$/);
	      $other .= "$SqlConfig{OTHER_ARG_SEP}$key=$val";
	    }
	}

	else
	{
	    my $meth = $SqlConfig{OTHER_ARGS};
	    my $val  = $self->$meth;
	    $other = "$SqlConfig{OTHER_ARG_SEP}$SqlConfig{OTHER_ARGS}=$val"
	      if defined $val;
	}
    }

    return "$dsn:$pre$name$other";
}

sub disconnect
{
    my $self = shift;

    if(exists $self->{'dbh'})
    {
	$self->dbh->disconnect;
	$self->is_connected = '';
	delete $self->{'dbh'};
    }
}

sub hash
{
    unless (@_==2) {
	Carp::confess qq(SqlObject: wrong number of arguments for hash);
    }

    my ($self,$query) = @_;

    my $sth = $self->prepare($query);
    $sth->execute();

    my $href = $sth->fetchrow_hashref;
    $sth->finish;

    return unless defined $href;
    return wantarray ? %$href : $href;
}

sub hashes
{
    if (@_<2 or @_>3) {
	Carp::confess "SqlObject: wrong number of arguments for hashes"
    }
    my ($self,$query,$cref) = @_;

    my $sth = $self->prepare($query); $sth->execute();

    if($cref)
    {
	my $href;
	$cref->($href) while $href = $sth->fetchrow_hashref;

	$sth->finish();
	return;
    } else
    {
	my @AoH = @{ $sth->fetchall_arrayref( {} ) };
	$sth->finish();

	return wantarray ? @AoH : \@AoH;
    }
}

sub array
{
    unless (@_==2) {
	Carp::confess "SqlObject: wrong number of arguments for array"
    }

    my $self  = shift;
    my $query = shift;

    my $sth = $self->prepare($query);

    if (my $err = $self->Error)
    {
	Carp::confess ("SqlObject: bad SQL for array \n",
		       "Query: '$query'\nError: '$err'")
    }

    $sth->execute();

    my @arr = $sth->fetchrow_array();
    $sth->finish;

    return wantarray ? @arr : \@arr;
}

#
# Execute the query and return a single element
#
sub value
{
    unless (@_==2) {
	Carp::confess "SqlObject: wrong number of arguments for value"
    }

    my $self  = shift;
    my $query = shift;

    my $sth = $self->prepare($query);

    if (my $err = $self->Error)
    {
	Carp::confess ("SqlObject: bad SQL for value \n",
		       "Query: '$query'\nError: '$err'")
    }

    $sth->execute();

    my ($val) = $sth->fetchrow_array;
    $sth->finish;

    return $val;
}

#
# Execute the query and return the first element of each result row
#
sub list {
    unless (@_==2) {
	Carp::confess "SqlObject: wrong number of arguments for list"
    }

  my $self  = shift;
  my $query = shift;

  my $sth = $self->prepare($query);

    if (my $err = $self->Error)
    {
	Carp::confess ("SqlObject: bad SQL for list \n",
		       "Query: '$query'\nError: '$err'")
    }

  $sth->execute();

  my @vals;
  push @vals, $_ while ($_) = $sth->fetchrow_array;
  $sth->finish;

  return wantarray ? @vals : \@vals;
}


sub delete {

    unless (@_>1 and @_<4)
    {
	Carp::confess "SqlObject: wrong number of arguments for delete"
    }

    my ($self,$table,$href) = @_;
    my ($err, $query);

    $self->_sql_quote_hash($href) if defined $href and ref $href;

    $query = $self->_sql_delete_query($table,$href);
    $self->do($query);
    $err = $self->Error();

    if ($err = $self->Error)
    {
	Carp::confess ("SqlObject: delete error\n",
		       "Query: '$query' Error: '$err'")
    }
}

sub insert
{
    unless (@_==3)
    {
	Carp::confess "SqlObject: wrong number of arguments for insert"
    }

    my ($self, $table, $href) = @_;
    my ($err, $query);

    $self->_sql_quote_hash($href);

    $query = $self->_sql_insert_query($table,$href);
    $self->do($query); $err = $self->Error();

    if ($err = $self->Error)
    {
	Carp::confess ("SqlObject: insert error\n",
		       "Query: '$query'\nError: '$err'")
    }
}

sub cond_insert {
    unless (@_>2 and @_<4)
    {
	Carp::confess "SqlObject: wrong number of arguments for insert"
    }

    my ($self, $table, $href, $whref) = @_;
    my ($found,$err, $exists_query);

    $self->_sql_quote_hash($href);

    if ($whref)
    {
	$self->_sql_quote_hash($href);
	$exists_query =
	    $self->_sql_select_query($table,[keys %$whref], $whref);
    } else
    {
	$exists_query =
	    $self->_sql_select_query($table, [keys %$href], $href);
    }

    $found = $self->value($exists_query);
    $err = $self->Error();

    if ($err)
    {
	Carp::confess ("SqlObject cond_insert exists error\n",
		       "Query: $exists_query\nError: $err")
    }

    return if $found;

    my $insert_query = $self->_sql_insert_query($table,$href);
    $self->do($insert_query);
    $err = $self->Error();
    if ($err)
    {
	Carp::confess ("SqlObject insert error\n",
		       "Query: $insert_query\nError: $err")
    }

    return 1;
}


sub insert_select {

    unless (@_>2 and @_ < 5)
    {
	Carp::confess "SqlObject: wrong number of arguments for insert_select"
    }

    my ($self, $table, $href, $column) = @_;
    my ($err, $insert_query);

    $column ||= join '_', $table, 'id';
    $column =~  s/^.*?(\w+)_id$/$1_id/;

    $self->_sql_quote_hash($href);

    $insert_query = $self->_sql_insert_query($table,$href);
    $self->do($insert_query);
    $err = $self->Error();

    if ($err)
    {
	Carp::confess ("SqlObject insert_select insert error\n",
		       "Query: $insert_query\nError: $err")
    }

    my $select_query = $self->_sql_select_query($table,[$column],$href);
    return $self->value($select_query);
}

sub cond_insert_select {

    unless (@_>2 and @_<6)
    {
	Carp::confess "SqlObject: wrong number of arguments for cond_insert_select"
    }

    my ($self, $table, $href, $arg4, $arg5) = splice @_, 0, 3;
    my ($whref,$column, $found, $err, $exists_query);

    $self->_sql_quote_hash($href);

    if (@_ == 1)
    {
	# we have whref or column but not both
	if (ref $_[0])
	{
	    $whref  = shift;
	}

        else
	{
	    $column = shift;
	}
    }
    elsif (@_ ==2) 
    {
	# we have both	$whref = shift;
	$column = shift;
	last;
    }
    else
    {
	# we have neither
	$column = join '_',$table,'id';
    }

    if ($whref)
    {
	$self->_sql_quote_hash($href);
	$exists_query = $self->_sql_select_query($table, [$column], $whref);
    }
    else
    {
	$exists_query = $self->_sql_select_query($table, [$column], $href);
    }

    $found = $self->value($exists_query);
    $err = $self->Error();

    if ($err)
    {
	Carp::confess ("SqlObject cond_insert_select exists error\n",
		       "Query: $exists_query\nError: $err")
    }

    return $found if $found;

    my $insert_query = $self->_sql_insert_query($table,$href);
    my $select_query = $self->_sql_select_query($table,[$column],$href);

    $self->do($insert_query);
    $err = $self->Error();

    if ($err)
    {
	Carp::confess ("SqlObject insert_select insert error\n",
		       "Query: $insert_query\nError: $err")
    }

    return $self->value($select_query);
}

sub update
{

    unless (@_>2 and @_<5)
    {
	Carp::confess "SqlObject: wrong number of arguments for update"
    }

    my ($self, $table, $shref, $whref) = @_;
    my($err, $query);


    $self->_sql_quote_hash($shref);
    $self->_sql_quote_hash($whref) if defined $whref && ref $whref;

    $query  = $self->_sql_update_query($table,$shref,$whref);
    $self->do($query);
    $err = $self->Error();

    if ($err)
    {
	Carp::confess ("SqlObject update error\n",
		       "Query: $query\nError: $err")
    }
}


## Here be the private methods

sub _sql_quote_hash
{
    my $href = pop;

    while ( my ($k,$v) = each %$href)
    {
	$v =~ s|^'||;
	$v =~ s|'$||;
	$v =~ s|'|''|g;
	$v =  qq('') if $v=~/^\s*$/;
	$v =  qq('$v') if $v=~/[^0-9]/ and $v!~/^null$/i;
	$href->{$k}=$v;
    }
}

sub _sql_insert_query
{
    return unless @_ > 2;

    my ($self, $table, $href) = @_;

    my ($columns,$values);
    while (my ($k,$v)=each %$href)
    {
	$columns .= "$k,";
	$values  .= "$v,";
    }
    $columns =~ s|,$||;
    $values  =~ s|,$||;

    return qq(insert into $table ($columns) values($values));
}

sub _sql_select_query {
    return unless @_ > 2;

    my ($self,$table,$aref,$href) = @_;
    my $columns = join ',',@$aref;
    my $where;

    while (my ($k,$v)=each %$href)
    {
	    $where .= qq($k = $v and );
    }
    $where =~ s|\s*and\s*$||;

    return qq(select $columns from $table where $where);
}

sub _sql_delete_query
{
    return unless @_ > 1;

    my ($self, $table, $href) = @_;
    return "delete from $table" if $href == undef;

    my $where;
    while (my ($k,$v)=each %$href) {
	$where .= qq($k = $v and );
    }
    $where =~ s|\s*and\s*$||;

    return qq(delete from $table where $where);
}

sub _sql_update_query {
    return unless @_ > 2;

    my ($self, $table, $shref, $whref) = @_;

    my ($set,$where);
    while (my ($k,$v)=each %$shref)
    {
	$set .= qq($k = $v, );
    }
    $set =~ s|\s*,\s*$||;

    if (ref $whref)
    {
	while (my ($k,$v)=each %$whref)
        {
	    $where .= qq($k = $v and );
	}
	$where=~ s|\s*and\s*$||;
	$where = "where $where";
    }

    return qq(update $table set $set $where);
}

our $AUTOLOAD;
sub AUTOLOAD
{
    $AUTOLOAD =~ /::([a-zA-Z_][a-zA-Z_0-9]+)$/;
    my $func = $1 or do
    {
	my @caller = caller;
	die (qq[Database Handle unable to dispatch.\n],
	     qq[Method $AUTOLOAD called by $caller[1] line $caller[2].\n]);
    };

    my $self = shift;
    my $result;

    if (my $dbh  = eval { $self->dbh })
    {
	$result = eval { $dbh->$func(@_) };
    }

    if ($@)
    {
	# error while calling DBI function
	my @caller = caller;
	my $err = ref $_[0] && UNIVERSAL::isa($_[0],__PACKAGE__) 
	          ? $_[0]->Error
		  : 'no object';
	die (qq[Database Handle encountered an error executing $func.\n],
	     qq[$AUTOLOAD called by $caller[1] line $caller[2].\n],
	     qq[Error: $@.\n],
	     qq[DBI Error: $err.\n]);
    }

    return $result;
}

1;

__END__

=pod

=head1 NAME

SqlObject - Sql module for wrappers around DBI

=head1 SYNOPSIS

    use SQL::SqlObject;

    $dbh = new SQL::SqlObject('my_db','dbi::pg','user','passwd');

    $dbh->db_dsn      = $dsn;    $dsn    = $dbh->db_dsn;
    $dbh->db_name     = $name;   $name   = $dbh->db_name;
    $dbh->db_user     = $user;   $usr    = $dbh->db_user;
    $dbh->db_password = $passwd; $passwd = $dbh->db_password;

    $dbh->connect([$SCALAR]); # Defaults to 'cezb-html'
    $dbh->disconnect();

    $SCALAR          = $dbh->value($SCALAR);
    [@LIST|$LISTREF] = $dbh->list($SCALAR);
    [@LIST|$LISTREF] = $dbh->array($SCALAR);
    [%HASH|$HASHREF] = $dbh->hash($SCALAR);
    [@AOH|$LISTREF]  = $dbh->hashes($SCALAR);

    $SCALAR = $dbh->insert_select($SCALAR,$HASHREF,[$SCALAR]);
    $dbh->insert($SCALAR,$HASHREF);
    $dbh->cond_insert($SCALAR,$HASHREF,[$HASHREF]);
    $SCALAR = $dbh->cond_insert_select($SCALAR,$HASHREF,[$HASHREF],[$SCALAR]);
    $dbh->delete($SCALAR,$HASHREF);
    $dbh->update($SCALAR,$HASHREF,[$HASHREF|$SCALAR]);

=head1 DESCRIPTION

The B<SQL::SqlObject> module allows you to use the B<DBI> module
with a hashref-based interface to the data.

Additionaly, as a wrapper module, calls on the B<SQL::SqlObject> instance
object which refer to a native L<DBI> method are passed through to the
underlying B<DBI> object.

Basicly, this module provides several functions to the underlying
B<DBI> object which are of great practical convience, particularly
when use B<DBI> under B<CGI>.

=head1 ABSTRACT

This program provides a set of utility functions to extend the
functionality of an underlying L<DBI> object around which the
B<SQL::SqlObject> is 'wrapped'.

This is intended to ease the delevopment of SQL intensive
applications.

This is B<not> intended as a replacement for Tim Bunce's L<DBI>
module, nor is it intended to surplant a full understanding of that
module, which the authors of the program consider a B<critical must> for
database programing in perl.

If you have not read the documentation for L<Tim Bunce's DBI
module|DBI>, or are not I<very> familiar with that document please
take take time to read it now.

Each B<SQL::SqlObject> instance object relies on an underly L<DBI>
object, the full functionality of which is available through the
B<SQL::SqlObject> wrapper.

=head1 EXAMPLES

    use SQL::SqlObject;

    # create an instance object connected to my_db
    $dbh = new SQL::SqlObject ("my_db");

    # print the number of rows in the table 'name'.
    print $dbh->value("select count(*) from name");

    # Print all of the first names in the table 'name' separated by
    # HTML line breaks.
    print join '<br>',$dbh->list("select first_name from name");

    # Print a list of all of the columns followed by the appropriate
    # data for a specific last name separated by HTML line breaks.
    %h = $dbh->hash("select * from name where last_name is 'Goff'");
    for (keys %h)) {
      print "$_: $h{$_}<br>";
    }

    # Print all of the names in the table 'name' separated by
    # HTML line breaks.
    for ($dbh->hashes("select first_name,last_name from name")) {
      print "$_->{first_name} $_->{last_name}<br>";
    }
    sub callback { print join '', map { "$_ => $h->{$_}\n" } shift }
    $dbh->hashes('select first_name,last_name from name',\&callback);

    # Insert data into the 'name' table
    %h = (first_name => 'Jeff', last_name => 'Goff');
    $dbh->insert('name',\%h);

    # Insert data into the 'name' table where an exactly
    # matching record doesn't already exist
    %h = (first_name => 'Corwin', last_name => 'Brust');
    if ($dbh->cond_insert('name',\%h)) {
      print "record was inserted"
    } else {
      print "record already existed"
    }

    # don't insert if a partial match is found
    %oh = (first_name => 'Corwin');
    $dbh->cond_insert('name',\%h, \%oh)

    # Insert data into the 'name' table and return
    # the 'name_id' field for the new row
    %h = (first_name => 'Someone' => last_name => 'New');
    $id = $dbh->insert_select('name',\%h);

    # same thing
    $id = $dbh->insert_select('name',\%h, 'name_id');

    # Insert data into the name table unless a matching
    # record is found.  Return 'name_id' for the existing
    # or inserted record
    %h = (first_name => 'Another' last_name => 'Newbie');
    $id = $dbh->cond_insert_select('name',\%h);

    # same thing
    $id = $dbh->cond_insert_select('name',\%h,'name_id');

    # partial match
    %oh = { first_name => 'Another' };
    $id = $dbh->cond_insert_select('name',\%h, \%oh);

    # same thing
    $id = $dbh->cond_insert_select('name',\%h, \%oh, 'name_id');

    # Delete a record from the 'name' table
    %h = (first_name => 'John', last_name => 'Public');
    $dbh->delete('name',\%h);

    # Update the 'Jeff Goff' record with a new business phone number
    %old = (first_name => 'Jeff', last_name => 'Goff');
    %new = (first_name => 'Jeff', last_name => 'Goff', bus_phone => '786-9601');
    $dbh->update('name',\%new,\%old);

=head1 CONSTRUCTOR

  $dbh = new SQL::SqlObject( [ SCALAR, [ SCALAR, [ SCALAR , [ SCALAR ] ] ]);

Creates an instance object of B<SQL::SqlObject> class, connected to
a database.

Arguments are as follows, each having a corrisponding
L<accessor|"ACCESSORS"> method.

=over

=item *
database name

The name of the database to which a connection should be made.

=item *
driver name (dsn)

The name of database driver to be used, in the format specified by the
L<BDI> module.

e.g.  dbi::pg

=item *
user name

The name of the user as which B<SQL::SqlObject> will attempt make it's
database connection.

NOTE: This is for the database's purposes only. This B<does not>
attempt any change in the effective user id under which a program
using B<SQL::SqlObject> is run.

=item *
password

The password for B<user name>, above, to be used in establishing
a database connection.

NOTE: Again, this is the password for the B<database user>, not a
system password.

=back

=head1 ACCESSORS

These methods provide access to the internal data stored by
B<SQL::SqlObject> instance objects.

=head2 dbh

  $dbh->dbh

Provides access to the underlying L<DBI> object.

The database handle (L<DBI>) is created the first time it is used, so
don't try somthing like:

  # This always works, if the module is 
  # properly installed and configured
  print "Connected" if $dbh->dbh;

To test if your B<SQL::SqlObject> instance object is connected the
L<DBI> which it is configured to wrap (eg: it has been used, since
the instance object was created) use this:

  print "Connected" if $dbh->is_connected;

NOTE: This is provided for the sake of completeness, and should not be
assigned to except possably by a program sub-classing this module
(L<See sub-classing|"SUB-CLASSING">).

=head2 db_name

  $name = $dbh->db_name;
  $dbh->db_name = 'my_database';

The name of database to which we'll be connecting.

=head2 db_user

  $user = $dbh->db_user;
  $dbh->db_user = 'perl_db_user';

The name of the database user for us to connect as.

=head2 db_password

  $passwd = $dbh->db_password;
  $dbh->db_password = 'perl_db_user_password';

The database password for the user as which we are connecting.

=head2 db_dsn

  $dsn = $dbh->db_dsn;
  $dbh->db_dsn = 'dbi::Sybase';

=head1 METHODS

The following subrutines are public methods available to instance
objects of the B<SQL::SqlObject> class.

=head2 is_connected

  $bool = $dbh->is_connected

Returns $bool will contain the value C<1> (one) if the database handle
has been invoked since the B<SQL::SqlObject> instance object was
created.

=head2 value

  SCALAR = $dbh->value( SCALAR )

Given a SQL SELECT statement return the first value of the result set
returned by the database after running that SQL.

  $sql = 'select first_name from name order by first_name limit 1';
  $firstfirst = $dbh->value($sql);

=head2 array

  LIST | LISTREF = $dbh->array( SCALAR )

Given a SQL SELECT statement return all values returned by that
statement after running that SQL, as a list or list reference.

This effectivly provides one step access to the C<fetchrow_arrayref>
method provided to B<DBI>'s L<statement handles|DBI/"DBI STATEMENT
HANDLE OBJECTS">. See L<Statement Handle Methods|DBI/"Statement Handle
Methods"> in the L<DBI> documentation for more information on the
C<fetchrow_arrayref> method.

=head2 list

  LIST | LISTREF = $dbh->list( SCALAR )

Given a SQL statement return the first value from all rows returned by
the database after running that SQL.

  $sql = 'select first_name from name order by first_name';
  @list = $dbh->list($sql);
  $listref = $dbh->list($sql);

=head2 hash

  HASH | HASHREF = $dbh->hash( SCALAR )

Given a SQL SELECT statement return all field names and values
returned by the database after running that SQL as a hash or hash
reference.

This effectivly provides one step access to the C<fetchrow_hashref>
method provided to B<DBI>'s L<statement handles|DBI/"DBI STATEMENT
HANDLE OBJECTS">. See L<Statement Handle Methods|DBI/"Statement Handle
Methods"> in the L<DBI> documentation for more information on the
C<fetchrow_hashref> method.

  my $sql = "select * from names where name_id = 1";
  %hash = $dbh->hash($sql);
  while my ($k, $v) (each %hash) {
    print "Field: $k\t";
    print "Value: $v\n";
  }

  $hashref = $dbh->hash($sql);
  printf "Field: %-20s\t%%s\n" $_, $hashref->{$_} for keys %$hashref;

=head2 hashes

  LIST | LISTREF = $dbh->hashes( SCALAR )

Given a SQL SELECT statement return all field names and values
returned by the database after running that SQL as a list of hash
references, or reference to a list of hash references.

Like the L<hash|"hash"> method, above, but returns all database rows
(where L<hash|"hash"> will return data from -at most- one row).

  my $sql = "select * from names order by name_id";
  @AoH = $dbh->hashes($sql);
  for my ($href) (@AoH) {
    print "$href->{last_name}, $href->{first_name}\n";
  }

  $listref = $dbh->hashes($sql);
  print map {
    "$listref->[$_]->{first_name} $listref->[$_]->{last_name}"
  } for 0..$#  $listref;

=head2 insert

  $dbh->insert( SCALAR, HASHREF);

Given a table name and a reference to an hash of field names and
values, perform a SQL INSERT query.

  %data = (first_name => 'Larry', last_name => 'Wall');
  $dbh->insert('name',\%data);

=head2 cond_insert

  SCALAR = $dbh->cond_insert( SCALAR, HASHREF [, HASHREF ])

Given a B<table name> and a reference to an hash of field names and
values, perform a SQL INSERT query unless a record exists already
exists in the specified table matching all values given in the hash
reference.

  %data = (first_name => 'Tim', last_name => 'Bunce');
  $bool_did_insert = $dbh->cond_insert('name',\%data);

If a second hash reference is provided, no C<INSERT QUERY> is
performed if a record can be found which exactly matches the values
provided for the fields listed therein.

  %data = (first_name => 'Tim', last_name => 'Bunce');
  %check = (last_name => 'Bunce');
  $bool_did_insert = $dbh->cond_insert('name',\%data, \%check);

If an C<INSERT QUERY> was performed, B<cond_insert> returns 1,
otherwise no return value is defined.

=head2 insert_select

  SCALAR = $dbh->insert_select( SCALAR, %HREF [, SCALAR] );

Given a B<table name>; a reference to an hash of field names and
values; and (optionaly) a B<field name> to select on success: Perform
a SQL C<INSERT QUERY> and select B<field name> from the newly inserted
row. 

If no B<field name> is provided a value of "<table>_id" is assumed.

  %data = (first_name => 'Bruce', last_name => 'Banner');
  $name_id = $dbh->insert_select('name',\%data);

  # get the 'create_time' field, after insert...
  %data = (first_name => 'Peter' last_name => 'Parker');
  $ctime = $dbh->insert_select('name',\%data,'create_time');

=head2 cond_insert_select

  SCALAR = $dbh->cond_insert_select( SCALAR, HASHREF [, HASHREF] [, SCALAR ])

Given a B<table name>; a reference to an hash of field names and
values; (optionaly) a reference to a second hash of field names and
values; and (optionaly) a B<field name>: Perform a SQL C<INSERT QUERY>
and select B<field name> from the newly inserted row B<only> if a row
matching all values from the second hash reference (or the first, if
only one was provided) cannot be found within the given table, then
return B<field name>.

As with L<cond_insert|"cond_insert">, if no B<field name> is provided
a value of "<table>_id" is assumed.

B<cond_insert_select> returns B<field name>

  %data = (first_name => 'Pappa', last_name => 'Smurf');
  $name_id = $dbh->cond_insert_select('name',\%data);


  # get name.name_id for a record where last_name = 'Ock'
  # or insert a new record for Doc Ock and get the name_id
  # for the new row.
  %data = (first_name => 'Doc', last_name => 'Ock');
  %where = (last_name => 'Ock');
  $name_id = $dbh->cond_insert_select('name',\%data,\%where);

  # insert a record for marilyn mason (unless one already
  # exists).  In either case, get the create_date for
  # marilyn's record in the 'name' table.
  %data = (first_name => 'Marilyn', last_name => 'Mason');
  $cdate = $dbh->cond_insert_select('name',\%data,'create_date');

  # insert a record for Doc Smith (unless there is already a record
  # where first_name = 'Doc').  Return the last_name field of the
  # record matched or inserted.
  %data = (first_name => 'Doc', last_name => 'Smith');
  %where = (first_name => 'Doc');
  $last = $dbh->cond_insert_select('name',\%data,\%where,'last_name');
  if ($last eq 'Smith') { print "inserted Doc Smith !" }
  else                  { print "found Doc $last !"    }

=head2 update

  $dbh->update( SCALAR, HASHREF,  SCALAR | HASHREF)

Given a B<table name>; a reference to a hash of field names and
values; and either a SQL where clause or a reference to a second hash
of field names and values: Update B<table name>.

  # set last_name = 'Smith' where first_name = 'Doc'
  %data = { last_name => 'Smith' };
  $dbh->update('name', \%data, q/where first_name = 'Doc'/);

  # set first_name = 'Bob' where last_name = 'Smith'
  %data = ( first_name => 'Bob' );
  %where = { last_name => 'Smith' };
  $dbh->update('name',\%data, \%where);

=head2 delete

  $dbh->delete (SCALAR, HASHREF)

Given a B<table name> and a reference to a hash of field names and
values, delete records from B<table name> which match hold values
currisponding to those in the provided hash reference for fields
specified by the keys of that hash reference.

  %data = (first_name => 'Bob', last_name => 'Smith');
  $dbh->delete('name',\%data);

=head1 SUB-CLASSING

In using this module, for our own nefarious purposes, we have found
that providing the various server/project specific data is often most
easily accomplished by creating a per server/project subclass of the
B<SQL::SqlObject> module.

This is quite easy to accomplish, and though TMTOWTDI certialy rules
our universe, the following should provide you with a good start to
doing this for your own needs.

  package MySqlProject;

  use strict;
  use warnings;
  use SQL::SqlObject;
  use base 'SQL::SqlObject';

  # post constructor processing
  sub _init {
    my ($self) = @_;
    $self->db_dsn      = 'dbi:mysql';     # DBD drive
    $self->db_name     = 'mysqldatabase'; # db name
    $self->db_name     = 'myclient';      # db username
    $self->db_password = 'mypassword';    # db password
  }

  __END__

  =head1 SEE ALSO

  L<SQL::SqlObject> - our base class

This allows you to write a script like

  #/bin/perl -w
  #
  # List of sometable entries, seperated by a blank line
  #
  use strict;
  use MySqlProject;
  my $dbh = new MySqlProject;
  for my $hashref ($dbh->hashes("select * from sometable"))
  {
      while (my ($col_name, $value) = each %$hashref)
      {
	  print $col_name, '.' x 20 - length $col_name, $value, "\n";
      }
      print "\n";
  }

Compare that to the following exactly equilivent example which doesn't
make use of the subclass.

  #/bin/perl -w
  #
  # List of sometable entries, seperated by a blank line
  #
  use strict;
  use MySqlProject;
  my $dbh = new MySqlProject(
                              --name     => 'mysqldatabase',
                              --user     => 'myclient',
                              --password => 'mypassword'
                             );
  $dbh->db_dsn = 'dbi::mysql';
  for my $hashref ($dbh->hashes("select * from sometable"))
  {
    ...


As you can see, there are only a few lines of difference, however,
consider the need to reapeat this in every script which makes a
database connection the convience of the former approach becomes
clear.

=head1 SEE ALSO

=over

=item *
L<DBI>

=item *
perl(1)

=back

=head1 NOTE

SQL::SqlObject may be redistributed under the same terms as Perl.

=head1 AUTHOR

The SqlObject interface was written by
Jeff Goff (E<lt>jgoff@hargray.comE<gt>) and
Corwin Brust (E<lt>cbrust@mpls.cxE<gt>)

=cut