package Jifty::DBI::Handle; use strict; use Carp (); use DBI (); use Class::ReturnValue (); use Encode (); use base qw/Jifty::DBI::HasFilters/; use vars qw(%DBIHandle $PrevHandle $DEBUG $TRANSDEPTH); $TRANSDEPTH = 0; our $VERSION = '0.01'; if ( my $pattern = $ENV{JIFTY_DBQUERY_CALLER} ) { require Hook::LexWrap; Hook::LexWrap::wrap( 'Jifty::DBI::Handle::simple_query', pre => sub { return unless $_[1] =~ m/$pattern/; Carp::cluck($_[1] . ' ' . CORE::join( ',', @_[ 2 .. $#_ ] )); } ); } =head1 NAME Jifty::DBI::Handle - Perl extension which is a generic DBI handle =head1 SYNOPSIS use Jifty::DBI::Handle; my $handle = Jifty::DBI::Handle->new(); $handle->connect( driver => 'mysql', database => 'dbname', host => 'hostname', user => 'dbuser', password => 'dbpassword'); # now $handle isa Jifty::DBI::Handle::mysql =head1 DESCRIPTION This class provides a wrapper for DBI handles that can also perform a number of additional functions. =cut =head2 new Generic constructor =cut sub new { my $proto = shift; my $class = ref($proto) || $proto; my $self = {}; bless( $self, $class ); @{ $self->{'StatementLog'} } = (); return $self; } =head2 connect PARAMHASH Takes a paramhash and connects to your DBI datasource, with the keys C, C, C, C and C. If you created the handle with Jifty::DBI::Handle->new and there is a Jifty::DBI::Handle::(Driver) subclass for the driver you have chosen, the handle will be automatically "upgraded" into that subclass. If there is an error, an exception will be thrown. If a connection has already been established and is still active, C will be returned (which is not an error). Otherwise, if a new connection is made, a true value will be returned. =cut sub connect { my $self = shift; my %args = ( driver => undef, database => undef, host => undef, sid => undef, port => undef, user => undef, password => undef, requiressl => undef, extra => {}, @_ ); if ( $args{'driver'} && !$self->isa( 'Jifty::DBI::Handle::' . $args{'driver'} ) ) { if ( $self->_upgrade_handle( $args{'driver'} ) ) { return ( $self->connect(%args) ); } } my $dsn = $self->dsn || ''; # Setting this actually breaks old RT versions in subtle ways. So we need to explicitly call it $self->build_dsn(%args); # Only connect if we're not connected to this source already if ( ( !$self->dbh ) || ( !$self->dbh->ping ) || ( $self->dsn ne $dsn ) ) { my $handle = DBI->connect( $self->dsn, $args{'user'}, $args{'password'}, $args{'extra'} ) || Carp::croak "Connection failed: $DBI::errstr\n"; #databases do case conversion on the name of columns returned. #actually, some databases just ignore case. this smashes it to something consistent $handle->{FetchHashKeyName} = 'NAME_lc'; #Set the handle $self->dbh($handle); return (1); } return (undef); } =head2 _upgrade_handle DRIVER This private internal method turns a plain Jifty::DBI::Handle into one of the standard driver-specific subclasses. =cut sub _upgrade_handle { my $self = shift; my $driver = shift; my $class = 'Jifty::DBI::Handle::' . $driver; local $@; eval "require $class"; return if $@; bless $self, $class; return 1; } =head2 build_dsn PARAMHASH Builds a dsn suitable for handing to DBI->connect. Mandatory arguments: =over =item driver =item database =back Optional arguments: =over =item host =item port =item sid =item requiressl =item and anything else your DBD lets you pass in =back =cut sub build_dsn { my $self = shift; my %args = ( driver => undef, database => undef, host => undef, port => undef, sid => undef, requiressl => undef, @_ ); my $driver = delete $args{'driver'}; $args{'dbname'} ||= delete $args{'database'}; delete $args{'user'}; delete $args{'password'}; delete $args{'extra'}; $self->{'dsn'} = "dbi:$driver:" . CORE::join( ';', map { $_ . "=" . $args{$_} } grep { defined $args{$_} } keys %args ); } =head2 dsn Returns the dsn for this database connection. =cut sub dsn { my $self = shift; return ( $self->{'dsn'} ); } =head2 raise_error [MODE] Turns on the Database Handle's RaiseError attribute. =cut sub raise_error { my $self = shift; $self->dbh->{RaiseError} = shift if (@_); return $self->dbh->{RaiseError}; } =head2 print_error [MODE] Turns on the Database Handle's PrintError attribute. =cut sub print_error { my $self = shift; $self->dbh->{PrintError} = shift if (@_); return $self->dbh->{PrintError}; } =head2 log MESSAGE Takes a single argument, a message to log. Currently prints that message to STDERR =cut sub log { my $self = shift; my $msg = shift; warn $msg . "\n"; } =head2 log_sql_statements BOOL Takes a boolean argument. If the boolean is true, it will log all SQL statements, as well as their invocation times and execution times. Returns whether we're currently logging or not as a boolean =cut sub log_sql_statements { my $self = shift; if (@_) { require Time::HiRes; $self->{'_dologsql'} = shift; } return ( $self->{'_dologsql'} ); } =head2 log_sql_hook NAME [, CODE] Used in instrumenting the SQL logging. You can use this to, for example, get a stack trace for each query (so you can find out where the query is being made). The name is required so that multiple hooks can be installed without stepping on eachother's toes. The coderef is run in scalar context and (currently) receives no arguments. If you don't pass CODE in, then the coderef currently assigned under NAME is returned. =cut sub log_sql_hook { my $self = shift; my $name = shift; if (@_) { $self->{'_logsqlhooks'}{$name} = shift; } return ( $self->{'_logsqlhooks'}{$name} ); } =head2 _log_sql_statement STATEMENT DURATION BINDINGS add an SQL statement to our query log =cut sub _log_sql_statement { my $self = shift; my $statement = shift; my $duration = shift; my @bind = @_; my %results; my @log = (Time::HiRes::time(), $statement, [@bind], $duration, \%results); while (my ($name, $code) = each %{ $self->{'_logsqlhooks'} || {} }) { $results{$name} = $code->(@log); } push @{ $self->{'StatementLog'} }, \@log; } =head2 clear_sql_statement_log Clears out the SQL statement log. =cut sub clear_sql_statement_log { my $self = shift; @{ $self->{'StatementLog'} } = (); } =head2 sql_statement_log Returns the current SQL statement log as an array of arrays. Each entry is a list of: (Time, Statement, [Bindings], Duration, {HookResults}) Bindings is an arrayref of the values of any placeholders. HookResults is a hashref keyed by hook name. =cut sub sql_statement_log { my $self = shift; return ( @{ $self->{'StatementLog'} } ); } =head2 auto_commit [MODE] Turns on the Database Handle's Autocommit attribute. =cut sub auto_commit { my $self = shift; my $mode = 1; $mode = shift if (@_); $self->dbh->{AutoCommit} = $mode; } =head2 disconnect disconnect from your DBI datasource =cut sub disconnect { my $self = shift; if ( $self->dbh ) { return ( $self->dbh->disconnect() ); } else { return; } } =head2 dbh [HANDLE] Return the current DBI handle. If we're handed a parameter, make the database handle that. =cut sub dbh { my $self = shift; #If we are setting the database handle, set it. $DBIHandle{$self} = $PrevHandle = shift if (@_); return ( $DBIHandle{$self} ||= $PrevHandle ); } =head2 delete $table_NAME @KEY_VALUE_PAIRS Takes a table name and a set of key-value pairs in an array. splits the key value pairs, constructs an DELETE statement and performs the delete. Returns the row_id of this row. =cut sub delete { my ( $self, $table, @pairs ) = @_; my @bind = (); my $where = 'WHERE '; while ( my $key = shift @pairs ) { $where .= $key . "=?" . " AND "; push( @bind, shift(@pairs) ); } $where =~ s/AND $//; my $query_string = "DELETE FROM " . $table . ' ' . $where; $self->simple_query( $query_string, @bind ); } =head2 insert $table_NAME @KEY_VALUE_PAIRS Takes a table name and a set of key-value pairs in an array. splits the key value pairs, constructs an INSERT statement and performs the insert. Returns the row_id of this row. =cut sub insert { my ( $self, $table, @pairs ) = @_; my ( @cols, @vals, @bind ); #my %seen; #only the *first* value is used - allows drivers to specify default while ( my $key = shift @pairs ) { my $value = shift @pairs; # next if $seen{$key}++; push @cols, $key; push @vals, '?'; push @bind, $value; } my $query_string = "INSERT INTO $table (" . CORE::join( ", ", @cols ) . ") VALUES " . "(" . CORE::join( ", ", @vals ) . ")"; my $sth = $self->simple_query( $query_string, @bind ); return ($sth); } =head2 update_record_value Takes a hash with columns: C, C, C, C, and C. The first two should be obvious; C is where you set the new value you want the column to have. The C column should be the lvalue of Jifty::DBI::Record::PrimaryKeys(). Finally , C is set when the Value is a SQL function. For example, you might have C<< value => 'PASSWORD(string)' >>, by setting C to true, that string will be inserted into the query directly rather then as a binding. =cut sub update_record_value { my $self = shift; my %args = ( table => undef, column => undef, is_sql_function => undef, primary_keys => undef, @_ ); return 1 unless grep {defined} values %{ $args{primary_keys} }; my @bind = (); my $query = 'UPDATE ' . $args{'table'} . ' '; $query .= 'SET ' . $args{'column'} . '='; ## Look and see if the column is being updated via a SQL function. if ( $args{'is_sql_function'} ) { $query .= $args{'value'} . ' '; } else { $query .= '? '; push( @bind, $args{'value'} ); } ## Constructs the where clause. my $where = 'WHERE '; foreach my $key ( keys %{ $args{'primary_keys'} } ) { $where .= $key . "=?" . " AND "; push( @bind, $args{'primary_keys'}{$key} ); } $where =~ s/AND\s$//; my $query_str = $query . $where; return ( $self->simple_query( $query_str, @bind ) ); } =head2 update_table_value table COLUMN NEW_value RECORD_ID IS_SQL Update column COLUMN of table table where the record id = RECORD_ID. If IS_SQL is set, don't quote the NEW_VALUE. =cut sub update_table_value { my $self = shift; ## This is just a wrapper to update_record_value(). my %args = (); $args{'table'} = shift; $args{'column'} = shift; $args{'value'} = shift; $args{'primary_keys'} = shift; $args{'is_sql_function'} = shift; return $self->update_record_value(%args); } =head2 simple_query QUERY_STRING, [ BIND_VALUE, ... ] Execute the SQL string specified in QUERY_STRING =cut sub simple_query { my $self = shift; my $query_string = shift; my @bind_values; @bind_values = (@_) if (@_); my $sth = $self->dbh->prepare($query_string); unless ($sth) { my $message = "$self couldn't prepare the query '$query_string': " . $self->dbh->errstr; if ($DEBUG) { die "$message\n"; } else { warn "$message\n"; my $ret = Class::ReturnValue->new(); $ret->as_error( errno => '-1', message => $message, do_backtrace => undef ); return ( $ret->return_value ); } } # Check @bind_values for HASH refs for ( my $bind_idx = 0; $bind_idx < scalar @bind_values; $bind_idx++ ) { if ( ref( $bind_values[$bind_idx] ) eq "HASH" ) { my $bhash = $bind_values[$bind_idx]; $bind_values[$bind_idx] = $bhash->{'value'}; delete $bhash->{'value'}; $sth->bind_param( $bind_idx + 1, undef, $bhash ); } # Some databases, such as Oracle fail to cope if it's a perl utf8 # string. they desperately want bytes. Encode::_utf8_off( $bind_values[$bind_idx] ); } my $basetime; if ( $self->log_sql_statements ) { $basetime = Time::HiRes::time(); } my $executed; local $@; { no warnings 'uninitialized'; # undef in bind_values makes DBI sad eval { $executed = $sth->execute(@bind_values) }; # try to ping and reconnect, if the DB connection failed if ($@ and !$self->dbh->ping) { $self->dbh(undef); # don't try pinging again, just connect $self->connect; eval { $executed = $sth->execute(@bind_values) }; } } if ( $self->log_sql_statements ) { $self->_log_sql_statement( $query_string, Time::HiRes::time() - $basetime, @bind_values ); } if ( $@ or !$executed ) { if ($DEBUG) { die "$self couldn't execute the query '$query_string'" . $self->dbh->errstr . "\n"; } else { # XXX: This warn doesn't show up because we mask logging in Jifty::Test::END. # and it usually fails because the test server is still running. warn "$self couldn't execute the query '$query_string'"; my $ret = Class::ReturnValue->new(); $ret->as_error( errno => '-1', message => "Couldn't execute the query '$query_string'" . $self->dbh->errstr, do_backtrace => undef ); return ( $ret->return_value ); } } return ($sth); } =head2 fetch_result QUERY, [ BIND_VALUE, ... ] Takes a SELECT query as a string, along with an array of BIND_VALUEs If the select succeeds, returns the first row as an array. Otherwise, returns a Class::ResturnValue object with the failure loaded up. =cut sub fetch_result { my $self = shift; my $query = shift; my @bind_values = @_; my $sth = $self->simple_query( $query, @bind_values ); if ($sth) { return ( $sth->fetchrow ); } else { return ($sth); } } =head2 blob_params COLUMN_NAME COLUMN_TYPE Returns a hash ref for the bind_param call to identify BLOB types used by the current database for a particular column type. =cut sub blob_params { my $self = shift; # Don't assign to key 'value' as it is defined later. return ( {} ); } =head2 database_version Returns the database's version. If argument C is true returns short variant, in other case returns whatever database handle/driver returns. By default returns short version, e.g. '4.1.23' or '8.0-rc4'. Returns empty string on error or if database couldn't return version. The base implementation uses a C