# $Header: /home/jesse/DBIx-SearchBuilder/history/SearchBuilder/Handle.pm,v 1.21 2002/01/28 06:11:37 jesse Exp $ package DBIx::SearchBuilder::Handle; use strict; use warnings; use Carp qw(croak cluck); use DBI; use Class::ReturnValue; use Encode qw(); use DBIx::SearchBuilder::Util qw/ sorted_values /; use vars qw(@ISA %DBIHandle $PrevHandle $DEBUG %TRANSDEPTH %TRANSROLLBACK %FIELDS_IN_TABLE); =head1 NAME DBIx::SearchBuilder::Handle - Perl extension which is a generic DBI handle =head1 SYNOPSIS use DBIx::SearchBuilder::Handle; my $handle = DBIx::SearchBuilder::Handle->new(); $handle->Connect( Driver => 'mysql', Database => 'dbname', Host => 'hostname', User => 'dbuser', Password => 'dbpassword'); # now $handle isa DBIx::SearchBuilder::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: Driver, Database, Host, User, Password Takes a paramhash and connects to your DBI datasource. You should _always_ set DisconnectHandleOnDestroy => 1 unless you have a legacy app like RT2 or RT 3.0.{0,1,2} that depends on the broken behaviour. If you created the handle with DBIx::SearchBuilder::Handle->new and there is a DBIx::SearchBuilder::Handle::(Driver) subclass for the driver you have chosen, the handle will be automatically "upgraded" into that subclass. =cut sub Connect { my $self = shift; my %args = ( Driver => undef, Database => undef, Host => undef, SID => undef, Port => undef, User => undef, Password => undef, RequireSSL => undef, DisconnectHandleOnDestroy => undef, @_ ); if ( $args{'Driver'} && !$self->isa( __PACKAGE__ .'::'. $args{'Driver'} ) ) { return $self->Connect( %args ) if $self->_UpgradeHandle( $args{'Driver'} ); } # Setting this actually breaks old RT versions in subtle ways. # So we need to explicitly call it $self->{'DisconnectHandleOnDestroy'} = $args{'DisconnectHandleOnDestroy'}; my $old_dsn = $self->DSN || ''; my $new_dsn = $self->BuildDSN( %args ); # Only connect if we're not connected to this source already return undef if $self->dbh && $self->dbh->ping && $new_dsn eq $old_dsn; my $handle = DBI->connect( $new_dsn, $args{'User'}, $args{'Password'} ) or croak "Connect 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); # Cache version info $self->DatabaseVersion; return 1; } =head2 _UpgradeHandle DRIVER This private internal method turns a plain DBIx::SearchBuilder::Handle into one of the standard driver-specific subclasses. =cut sub _UpgradeHandle { my $self = shift; my $driver = shift; my $class = 'DBIx::SearchBuilder::Handle::' . $driver; local $@; eval "require $class"; return if $@; bless $self, $class; return 1; } =head2 BuildDSN PARAMHASH Takes a bunch of parameters: Required: Driver, Database, Optional: Host, Port and RequireSSL Builds a DSN suitable for a DBI connection =cut sub BuildDSN { my $self = shift; my %args = ( Driver => undef, Database => undef, Host => undef, Port => undef, SID => undef, RequireSSL => undef, @_ ); my $dsn = "dbi:$args{'Driver'}:dbname=$args{'Database'}"; $dsn .= ";sid=$args{'SID'}" if $args{'SID'}; $dsn .= ";host=$args{'Host'}" if $args{'Host'}; $dsn .= ";port=$args{'Port'}" if $args{'Port'}; $dsn .= ";requiressl=1" if $args{'RequireSSL'}; return $self->{'dsn'} = $dsn; } =head2 DSN Returns the DSN for this database connection. =cut sub DSN { return shift->{'dsn'}; } =head2 RaiseError [MODE] Turns on the Database Handle's RaiseError attribute. =cut sub RaiseError { my $self = shift; my $mode = 1; $mode = shift if (@_); $self->dbh->{RaiseError}=$mode; } =head2 PrintError [MODE] Turns on the Database Handle's PrintError attribute. =cut sub PrintError { my $self = shift; my $mode = 1; $mode = shift if (@_); $self->dbh->{PrintError}=$mode; } =head2 LogSQLStatements BOOL Takes a boolean argument. If the boolean is true, SearchBuilder 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 LogSQLStatements { my $self = shift; if (@_) { require Time::HiRes; $self->{'_DoLogSQL'} = shift; } return ($self->{'_DoLogSQL'}); } =head2 _LogSQLStatement STATEMENT DURATION Add an SQL statement to our query log =cut sub _LogSQLStatement { my $self = shift; my $statement = shift; my $duration = shift; my @bind = @_; push @{$self->{'StatementLog'}} , ([Time::HiRes::time(), $statement, [@bind], $duration, Carp::longmess("Executed SQL query")]); } =head2 ClearSQLStatementLog Clears out the SQL statement log. =cut sub ClearSQLStatementLog { my $self = shift; @{$self->{'StatementLog'}} = (); } =head2 SQLStatementLog Returns the current SQL statement log as an array of arrays. Each entry is a triple of (Time, Statement, Duration) =cut sub SQLStatementLog { my $self = shift; return (@{$self->{'StatementLog'}}); } =head2 AutoCommit [MODE] Turns on the Database Handle's AutoCommit attribute. =cut sub AutoCommit { 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; my $dbh = $self->dbh; return unless $dbh; $self->Rollback(1); my $ret = $dbh->disconnect; # DBD::mysql with MariaDB 10.2+ could cause segment faults when # interacting with a disconnected handle, here we unset # dbh to inform other code that there is no connection any more. # See also https://github.com/perl5-dbi/DBD-mysql/issues/306 if ( $self->isa('DBIx::SearchBuilder::Handle::mysql') && $self->{'database_version'} =~ /mariadb/i && $self->{'database_version'} ge '10.2' ) { $self->dbh(undef); } return $ret; } =head2 dbh [HANDLE] Return the current DBI handle. If we're handed a parameter, make the database handle that. =cut # allow use of Handle as a synonym for DBH *Handle=\&dbh; sub dbh { my $self=shift; #If we are setting the database handle, set it. if ( @_ ) { $DBIHandle{$self} = $PrevHandle = shift; %FIELDS_IN_TABLE = (); } return($DBIHandle{$self} ||= $PrevHandle); } =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. Base class return statement handle object, while DB specific subclass should return row id. =cut sub Insert { my $self = shift; return $self->SimpleQuery( $self->InsertQueryString(@_) ); } =head2 InsertQueryString $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 returns query string and set of bind values. This method is more useful for subclassing in DB specific handles. L method is preferred for end users. =cut sub InsertQueryString { my($self, $table, @pairs) = @_; my(@cols, @vals, @bind); while ( my $key = shift @pairs ) { push @cols, $key; push @vals, '?'; push @bind, shift @pairs; } my $QueryString = "INSERT INTO $table"; $QueryString .= " (". join(", ", @cols) .")"; $QueryString .= " VALUES (". join(", ", @vals). ")"; return ($QueryString, @bind); } =head2 InsertFromSelect Takes table name, array reference with columns, select query and list of bind values. Inserts data select by the query into the table. To make sure call is portable every column in result of the query should have unique name or should be aliased. See L for details. =cut sub InsertFromSelect { my ($self, $table, $columns, $query, @binds) = @_; $columns = join ', ', @$columns if $columns; my $full_query = "INSERT INTO $table"; $full_query .= " ($columns)" if $columns; $full_query .= ' '. $query; my $sth = $self->SimpleQuery( $full_query, @binds ); return $sth unless $sth; my $rows = $sth->rows; return $rows == 0? '0E0' : $rows; } =head2 UpdateRecordValue Takes a hash with fields: Table, Column, Value PrimaryKeys, and IsSQLFunction. Table, and Column should be obvious, Value is where you set the new value you want the column to have. The primary_keys field should be the lvalue of DBIx::SearchBuilder::Record::PrimaryKeys(). Finally IsSQLFunction is set when the Value is a SQL function. For example, you might have ('Value'=>'PASSWORD(string)'), by setting IsSQLFunction that string will be inserted into the query directly rather then as a binding. =cut sub UpdateRecordValue { my $self = shift; my %args = ( Table => undef, Column => undef, IsSQLFunction => undef, PrimaryKeys => undef, @_ ); my @bind = (); my $query = 'UPDATE ' . $args{'Table'} . ' '; $query .= 'SET ' . $args{'Column'} . '='; ## Look and see if the field is being updated via a SQL function. if ($args{'IsSQLFunction'}) { $query .= $args{'Value'} . ' '; } else { $query .= '? '; push (@bind, $args{'Value'}); } ## Constructs the where clause. my $where = 'WHERE '; foreach my $key (sort keys %{$args{'PrimaryKeys'}}) { $where .= $key . "=?" . " AND "; push (@bind, $args{'PrimaryKeys'}{$key}); } $where =~ s/AND\s$//; my $query_str = $query . $where; return ($self->SimpleQuery($query_str, @bind)); } =head2 UpdateTableValue 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 UpdateTableValue { my $self = shift; ## This is just a wrapper to UpdateRecordValue(). my %args = (); $args{'Table'} = shift; $args{'Column'} = shift; $args{'Value'} = shift; $args{'PrimaryKeys'} = shift; $args{'IsSQLFunction'} = shift; return $self->UpdateRecordValue(%args) } =head1 SimpleUpdateFromSelect Takes table name, hash reference with (column, value) pairs, select query and list of bind values. Updates the table, but only records with IDs returned by the selected query, eg: UPDATE $table SET %values WHERE id IN ( $query ) It's simple as values are static and search only allowed by id. =cut sub SimpleUpdateFromSelect { my ($self, $table, $values, $query, @query_binds) = @_; my @columns; my @binds; for my $k (sort keys %$values) { push @columns, $k; push @binds, $values->{$k}; } my $full_query = "UPDATE $table SET "; $full_query .= join ', ', map "$_ = ?", @columns; $full_query .= ' WHERE id IN ('. $query .')'; my $sth = $self->SimpleQuery( $full_query, @binds, @query_binds ); return $sth unless $sth; my $rows = $sth->rows; return $rows == 0? '0E0' : $rows; } =head1 DeleteFromSelect Takes table name, select query and list of bind values. Deletes from the table, but only records with IDs returned by the select query, eg: DELETE FROM $table WHERE id IN ($query) =cut sub DeleteFromSelect { my ($self, $table, $query, @binds) = @_; my $sth = $self->SimpleQuery( "DELETE FROM $table WHERE id IN ($query)", @binds ); return $sth unless $sth; my $rows = $sth->rows; return $rows == 0? '0E0' : $rows; } =head2 SimpleQuery QUERY_STRING, [ BIND_VALUE, ... ] Execute the SQL string specified in QUERY_STRING =cut sub SimpleQuery { my $self = shift; my $QueryString = shift; my @bind_values; @bind_values = (@_) if (@_); my $sth = $self->dbh->prepare($QueryString); unless ($sth) { if ($DEBUG) { die "$self couldn't prepare the query '$QueryString'" . $self->dbh->errstr . "\n"; } else { warn "$self couldn't prepare the query '$QueryString'" . $self->dbh->errstr . "\n"; my $ret = Class::ReturnValue->new(); $ret->as_error( errno => '-1', message => "Couldn't prepare the query '$QueryString'." . $self->dbh->errstr, 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 ); } } my $basetime; if ( $self->LogSQLStatements ) { $basetime = Time::HiRes::time(); } my $executed; { no warnings 'uninitialized' ; # undef in bind_values makes DBI sad eval { $executed = $sth->execute(@bind_values) }; } if ( $self->LogSQLStatements ) { $self->_LogSQLStatement( $QueryString, Time::HiRes::time() - $basetime, @bind_values ); } if ( $@ or !$executed ) { if ($DEBUG) { die "$self couldn't execute the query '$QueryString'" . $self->dbh->errstr . "\n"; } else { cluck "$self couldn't execute the query '$QueryString'"; my $ret = Class::ReturnValue->new(); $ret->as_error( errno => '-1', message => "Couldn't execute the query '$QueryString'" . $self->dbh->errstr, do_backtrace => undef ); return ( $ret->return_value ); } } return ($sth); } =head2 FetchResult 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 FetchResult { my $self = shift; my $query = shift; my @bind_values = @_; my $sth = $self->SimpleQuery($query, @bind_values); if ($sth) { return ($sth->fetchrow); } else { return($sth); } } =head2 BinarySafeBLOBs Returns 1 if the current database supports BLOBs with embedded nulls. Returns undef if the current database doesn't support BLOBs with embedded nulls =cut sub BinarySafeBLOBs { my $self = shift; return(1); } =head2 KnowsBLOBs Returns 1 if the current database supports inserts of BLOBs automatically. Returns undef if the current database must be informed of BLOBs for inserts. =cut sub KnowsBLOBs { my $self = shift; return(1); } =head2 BLOBParams FIELD_NAME FIELD_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 BLOBParams { my $self = shift; # Don't assign to key 'value' as it is defined later. return ( {} ); } =head2 DatabaseVersion [Short => 1] 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