X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FDBIx%2FClass%2FStorage%2FDBI.pm;h=de6272eb11dec49fb288d820a55b1e9db3c2eb9d;hb=d5130dd2ecbb3a4d31393332f6b4e419f251f427;hp=415878188cccbb2e135038c04e183cdbf810ff03;hpb=093fc7a6ef41c585afecfbc3ed18e300e65a1cd5;p=dbsrgits%2FDBIx-Class.git diff --git a/lib/DBIx/Class/Storage/DBI.pm b/lib/DBIx/Class/Storage/DBI.pm index 4158781..de6272e 100644 --- a/lib/DBIx/Class/Storage/DBI.pm +++ b/lib/DBIx/Class/Storage/DBI.pm @@ -10,7 +10,13 @@ use SQL::Abstract::Limit; use DBIx::Class::Storage::DBI::Cursor; use DBIx::Class::Storage::Statistics; use IO::File; -use Carp::Clan qw/DBIx::Class/; + +__PACKAGE__->mk_group_accessors( + 'simple' => + qw/_connect_info _dbh _sql_maker _sql_maker_opts _conn_pid _conn_tid + cursor on_connect_do transaction_depth/ +); + BEGIN { package DBIC::SQL::Abstract; # Would merge upstream, but nate doesn't reply :( @@ -29,10 +35,36 @@ sub new { $self; } +sub _RowNumberOver { + my ($self, $sql, $order, $rows, $offset ) = @_; + + $offset += 1; + my $last = $rows + $offset; + my ( $order_by ) = $self->_order_by( $order ); + + $sql = <<""; +SELECT * FROM +( + SELECT Q1.*, ROW_NUMBER() OVER( ) AS ROW_NUM FROM ( + $sql + $order_by + ) Q1 +) Q2 +WHERE ROW_NUM BETWEEN $offset AND $last + + return $sql; +} + + # While we're at it, this should make LIMIT queries more efficient, # without digging into things too deeply sub _find_syntax { my ($self, $syntax) = @_; + my $dbhname = ref $syntax eq 'HASH' ? $syntax->{Driver}{Name} : ''; + if(ref($self) && $dbhname && $dbhname eq 'DB2') { + return 'RowNumberOver'; + } + $self->{_cached_syntax} ||= $self->SUPER::_find_syntax($syntax); } @@ -250,14 +282,6 @@ sub name_sep { } # End of BEGIN block -use base qw/DBIx::Class/; - -__PACKAGE__->load_components(qw/AccessorGroup/); - -__PACKAGE__->mk_group_accessors('simple' => - qw/_connect_info _dbh _sql_maker _sql_maker_opts _conn_pid _conn_tid - debug debugobj cursor on_connect_do transaction_depth/); - =head1 NAME DBIx::Class::Storage::DBI - DBI storage handler @@ -266,49 +290,22 @@ DBIx::Class::Storage::DBI - DBI storage handler =head1 DESCRIPTION -This class represents the connection to the database +This class represents the connection to an RDBMS via L. See +L for general information. This pod only +documents DBI-specific methods and behaviors. =head1 METHODS -=head2 new - =cut sub new { - my $new = {}; - bless $new, (ref $_[0] || $_[0]); + my $new = shift->next::method(@_); $new->cursor("DBIx::Class::Storage::DBI::Cursor"); $new->transaction_depth(0); - - $new->debugobj(new DBIx::Class::Storage::Statistics()); - - my $fh; - - my $debug_env = $ENV{DBIX_CLASS_STORAGE_DBI_DEBUG} - || $ENV{DBIC_TRACE}; - - if (defined($debug_env) && ($debug_env =~ /=(.+)$/)) { - $fh = IO::File->new($1, 'w') - or $new->throw_exception("Cannot open trace file $1"); - } else { - $fh = IO::File->new('>&STDERR'); - } - $new->debugfh($fh); - $new->debug(1) if $debug_env; $new->_sql_maker_opts({}); - return $new; -} -=head2 throw_exception - -Throws an exception - croaks. - -=cut - -sub throw_exception { - my ($self, $msg) = @_; - croak($msg); + $new; } =head2 connect_info @@ -369,6 +366,12 @@ Every time C is invoked, any previous settings for these options will be cleared before setting the new ones, regardless of whether any options are specified in the new C. +Important note: DBIC expects the returned database handle provided by +a subref argument to have RaiseError set on it. If it doesn't, things +might not work very well, YMMV. If you don't use a subref, DBIC will +force this setting for you anyways. Setting HandleError to anything +other than simple exception object wrapper might cause problems too. + Examples: # Simple SQLite connection @@ -410,63 +413,170 @@ Examples: ] ); +=cut + +sub connect_info { + my ($self, $info_arg) = @_; + + return $self->_connect_info if !$info_arg; + + # Kill sql_maker/_sql_maker_opts, so we get a fresh one with only + # the new set of options + $self->_sql_maker(undef); + $self->_sql_maker_opts({}); + + my $info = [ @$info_arg ]; # copy because we can alter it + my $last_info = $info->[-1]; + if(ref $last_info eq 'HASH') { + if(my $on_connect_do = delete $last_info->{on_connect_do}) { + $self->on_connect_do($on_connect_do); + } + for my $sql_maker_opt (qw/limit_dialect quote_char name_sep/) { + if(my $opt_val = delete $last_info->{$sql_maker_opt}) { + $self->_sql_maker_opts->{$sql_maker_opt} = $opt_val; + } + } + + # Get rid of any trailing empty hashref + pop(@$info) if !keys %$last_info; + } + + $self->_connect_info($info); +} + =head2 on_connect_do This method is deprecated in favor of setting via L. -=head2 debug +=head2 dbh_do -Causes SQL trace information to be emitted on the C object. -(or C if C has not specifically been set). +Arguments: $subref, @extra_coderef_args? -This is the equivalent to setting L in your -shell environment. +Execute the given subref using the new exception-based connection management. -=head2 debugfh +The first two arguments will be the storage object that C was called +on and a database handle to use. Any additional arguments will be passed +verbatim to the called subref as arguments 2 and onwards. -Set or retrieve the filehandle used for trace/debug output. This should be -an IO::Handle compatible ojbect (only the C method is used. Initially -set to be STDERR - although see information on the -L environment variable. +Using this (instead of $self->_dbh or $self->dbh) ensures correct +exception handling and reconnection (or failover in future subclasses). + +Your subref should have no side-effects outside of the database, as +there is the potential for your subref to be partially double-executed +if the database connection was stale/dysfunctional. + +Example: + + my @stuff = $schema->storage->dbh_do( + sub { + my ($storage, $dbh, @cols) = @_; + my $cols = join(q{, }, @cols); + $dbh->selectrow_array("SELECT $cols FROM foo"); + }, + @column_list + ); =cut -sub debugfh { - my $self = shift; +sub dbh_do { + my $self = shift; + my $coderef = shift; + + return $coderef->($self, $self->_dbh, @_) if $self->{_in_txn_do}; - if ($self->debugobj->can('debugfh')) { - return $self->debugobj->debugfh(@_); + ref $coderef eq 'CODE' or $self->throw_exception + ('$coderef must be a CODE reference'); + + my @result; + my $want_array = wantarray; + + eval { + $self->_verify_pid if $self->_dbh; + $self->_populate_dbh if !$self->_dbh; + if($want_array) { + @result = $coderef->($self, $self->_dbh, @_); } -} + elsif(defined $want_array) { + $result[0] = $coderef->($self, $self->_dbh, @_); + } + else { + $coderef->($self, $self->_dbh, @_); + } + }; -=head2 debugobj + my $exception = $@; + if(!$exception) { return $want_array ? @result : $result[0] } -Sets or retrieves the object used for metric collection. Defaults to an instance -of L that is campatible with the original -method of using a coderef as a callback. See the aforementioned Statistics -class for more information. + $self->throw_exception($exception) if $self->connected; -=head2 debugcb + # We were not connected - reconnect and retry, but let any + # exception fall right through this time + $self->_populate_dbh; + $coderef->($self, $self->_dbh, @_); +} -Sets a callback to be executed each time a statement is run; takes a sub -reference. Callback is executed as $sub->($op, $info) where $op is -SELECT/INSERT/UPDATE/DELETE and $info is what would normally be printed. +# This is basically a blend of dbh_do above and DBIx::Class::Storage::txn_do. +# It also informs dbh_do to bypass itself while under the direction of txn_do, +# via $self->{_in_txn_do} (this saves some redundant eval and errorcheck, etc) +sub txn_do { + my $self = shift; + my $coderef = shift; -See L for a better way. + ref $coderef eq 'CODE' or $self->throw_exception + ('$coderef must be a CODE reference'); -=cut + local $self->{_in_txn_do} = 1; -sub debugcb { - my $self = shift; + my @result; + my $want_array = wantarray; + + my $tried = 0; + while(1) { + eval { + $self->_verify_pid if $self->_dbh; + $self->_populate_dbh if !$self->_dbh; + + $self->txn_begin; + if($want_array) { + @result = $coderef->(@_); + } + elsif(defined $want_array) { + $result[0] = $coderef->(@_); + } + else { + $coderef->(@_); + } + $self->txn_commit; + }; - if ($self->debugobj->can('callback')) { - return $self->debugobj->callback(@_); + my $exception = $@; + if(!$exception) { return $want_array ? @result : $result[0] } + + if($tried++ > 0 || $self->connected) { + eval { $self->txn_rollback }; + my $rollback_exception = $@; + if($rollback_exception) { + my $exception_class = "DBIx::Class::Storage::NESTED_ROLLBACK_EXCEPTION"; + $self->throw_exception($exception) # propagate nested rollback + if $rollback_exception =~ /$exception_class/; + + $self->throw_exception( + "Transaction aborted: ${exception}. " + . "Rollback failed: ${rollback_exception}" + ); + } + $self->throw_exception($exception) } + + # We were not connected, and was first try - reconnect and retry + # via the while loop + $self->_populate_dbh; + } } =head2 disconnect -Disconnect the L handle, performing a rollback first if the +Our C method also performs a rollback first if the database is not in C mode. =cut @@ -481,22 +591,15 @@ sub disconnect { } } -=head2 connected - -Check if the L handle is connected. Returns true if the handle -is connected. - -=cut - -sub connected { my ($self) = @_; +sub connected { + my ($self) = @_; if(my $dbh = $self->_dbh) { if(defined $self->_conn_tid && $self->_conn_tid != threads->tid) { return $self->_dbh(undef); } - elsif($self->_conn_pid != $$) { - $self->_dbh->{InactiveDestroy} = 1; - return $self->_dbh(undef); + else { + $self->_verify_pid; } return ($dbh->FETCH('Active') && $dbh->ping); } @@ -504,12 +607,18 @@ sub connected { my ($self) = @_; return 0; } -=head2 ensure_connected +# handle pid changes correctly +# NOTE: assumes $self->_dbh is a valid $dbh +sub _verify_pid { + my ($self) = @_; -Check whether the database handle is connected - if not then make a -connection. + return if $self->_conn_pid == $$; -=cut + $self->_dbh->{InactiveDestroy} = 1; + $self->_dbh(undef); + + return; +} sub ensure_connected { my ($self) = @_; @@ -538,13 +647,6 @@ sub _sql_maker_args { return ( limit_dialect => $self->dbh, %{$self->_sql_maker_opts} ); } -=head2 sql_maker - -Returns a C object - normally an object of class -C. - -=cut - sub sql_maker { my ($self) = @_; unless ($self->_sql_maker) { @@ -553,37 +655,6 @@ sub sql_maker { return $self->_sql_maker; } -sub connect_info { - my ($self, $info_arg) = @_; - - if($info_arg) { - # Kill sql_maker/_sql_maker_opts, so we get a fresh one with only - # the new set of options - $self->_sql_maker(undef); - $self->_sql_maker_opts({}); - - my $info = [ @$info_arg ]; # copy because we can alter it - my $last_info = $info->[-1]; - if(ref $last_info eq 'HASH') { - if(my $on_connect_do = delete $last_info->{on_connect_do}) { - $self->on_connect_do($on_connect_do); - } - for my $sql_maker_opt (qw/limit_dialect quote_char name_sep/) { - if(my $opt_val = delete $last_info->{$sql_maker_opt}) { - $self->_sql_maker_opts->{$sql_maker_opt} = $opt_val; - } - } - - # Get rid of any trailing empty hashref - pop(@$info) if !keys %$last_info; - } - - $self->_connect_info($info); - } - - $self->_connect_info; -} - sub _populate_dbh { my ($self) = @_; my @info = @{$self->_connect_info || []}; @@ -622,9 +693,14 @@ sub _connect { } eval { - $dbh = ref $info[0] eq 'CODE' - ? &{$info[0]} - : DBI->connect(@info); + if(ref $info[0] eq 'CODE') { + $dbh = &{$info[0]} + } + else { + $dbh = DBI->connect(@info); + $dbh->{RaiseError} = 1; + $dbh->{PrintError} = 0; + } }; $DBI::connect_via = $old_connect_via if $old_connect_via; @@ -636,36 +712,23 @@ sub _connect { $dbh; } -=head2 txn_begin - -Calls begin_work on the current dbh. - -See L for the txn_do() method, which allows for -an entire code block to be executed transactionally. - -=cut +sub _dbh_txn_begin { + my ($self, $dbh) = @_; + if ($dbh->{AutoCommit}) { + $self->debugobj->txn_begin() + if ($self->debug); + $dbh->begin_work; + } +} sub txn_begin { my $self = shift; - if ($self->{transaction_depth}++ == 0) { - my $dbh = $self->dbh; - if ($dbh->{AutoCommit}) { - $self->debugobj->txn_begin() - if ($self->debug); - $dbh->begin_work; - } - } + $self->dbh_do($self->can('_dbh_txn_begin')) + if $self->{transaction_depth}++ == 0; } -=head2 txn_commit - -Issues a commit against the current dbh. - -=cut - -sub txn_commit { - my $self = shift; - my $dbh = $self->dbh; +sub _dbh_txn_commit { + my ($self, $dbh) = @_; if ($self->{transaction_depth} == 0) { unless ($dbh->{AutoCommit}) { $self->debugobj->txn_commit() @@ -682,38 +745,36 @@ sub txn_commit { } } -=head2 txn_rollback - -Issues a rollback against the current dbh. A nested rollback will -throw a L exception, -which allows the rollback to propagate to the outermost transaction. - -=cut - -sub txn_rollback { +sub txn_commit { my $self = shift; + $self->dbh_do($self->can('_dbh_txn_commit')); +} - eval { - my $dbh = $self->dbh; - if ($self->{transaction_depth} == 0) { - unless ($dbh->{AutoCommit}) { - $self->debugobj->txn_rollback() - if ($self->debug); - $dbh->rollback; - } +sub _dbh_txn_rollback { + my ($self, $dbh) = @_; + if ($self->{transaction_depth} == 0) { + unless ($dbh->{AutoCommit}) { + $self->debugobj->txn_rollback() + if ($self->debug); + $dbh->rollback; + } + } + else { + if (--$self->{transaction_depth} == 0) { + $self->debugobj->txn_rollback() + if ($self->debug); + $dbh->rollback; } else { - if (--$self->{transaction_depth} == 0) { - $self->debugobj->txn_rollback() - if ($self->debug); - $dbh->rollback; - } - else { - die DBIx::Class::Storage::NESTED_ROLLBACK_EXCEPTION->new; - } + die DBIx::Class::Storage::NESTED_ROLLBACK_EXCEPTION->new; } - }; + } +} + +sub txn_rollback { + my $self = shift; + eval { $self->dbh_do($self->can('_dbh_txn_rollback')) }; if ($@) { my $error = $@; my $exception_class = "DBIx::Class::Storage::NESTED_ROLLBACK_EXCEPTION"; @@ -723,22 +784,37 @@ sub txn_rollback { } } -sub _execute { +# This used to be the top-half of _execute. It was split out to make it +# easier to override in NoBindVars without duping the rest. It takes up +# all of _execute's args, and emits $sql, @bind. +sub _prep_for_execute { my ($self, $op, $extra_bind, $ident, @args) = @_; + my ($sql, @bind) = $self->sql_maker->$op($ident, @args); unshift(@bind, @$extra_bind) if $extra_bind; + @bind = map { ref $_ ? ''.$_ : $_ } @bind; # stringify args + + return ($sql, @bind); +} + +sub _execute { + my $self = shift; + + my ($sql, @bind) = $self->_prep_for_execute(@_); + if ($self->debug) { my @debug_bind = map { defined $_ ? qq{'$_'} : q{'NULL'} } @bind; $self->debugobj->query_start($sql, @debug_bind); } - my $sth = eval { $self->sth($sql,$op) }; + + my $sth = eval { $self->sth($sql) }; if (!$sth || $@) { $self->throw_exception( 'no sth generated via sql (' . ($@ || $self->_dbh->errstr) . "): $sql" ); } - @bind = map { ref $_ ? ''.$_ : $_ } @bind; # stringify args + my $rv; if ($sth) { my $time = time(); @@ -802,6 +878,12 @@ sub _select { =head2 select +=over 4 + +=item Arguments: $ident, $select, $condition, $attrs + +=back + Handle a SQL select statement. =cut @@ -812,55 +894,47 @@ sub select { return $self->cursor->new($self, \@_, $attrs); } -=head2 select_single - -Performs a select, fetch and return of data - handles a single row -only. - -=cut - -# Need to call finish() to work round broken DBDs - sub select_single { my $self = shift; my ($rv, $sth, @bind) = $self->_select(@_); my @row = $sth->fetchrow_array; + # Need to call finish() to work round broken DBDs $sth->finish(); return @row; } =head2 sth +=over 4 + +=item Arguments: $sql + +=back + Returns a L sth (statement handle) for the supplied SQL. =cut -sub sth { - my ($self, $sql) = @_; +sub _dbh_sth { + my ($self, $dbh, $sql) = @_; # 3 is the if_active parameter which avoids active sth re-use - return $self->dbh->prepare_cached($sql, {}, 3); + $dbh->prepare_cached($sql, {}, 3); } -=head2 columns_info_for - -Returns database type info for a given table columns. - -=cut - -sub columns_info_for { - my ($self, $table) = @_; +sub sth { + my ($self, $sql) = @_; + $self->dbh_do($self->can('_dbh_sth'), $sql); +} - my $dbh = $self->dbh; +sub _dbh_columns_info_for { + my ($self, $dbh, $table) = @_; if ($dbh->can('column_info')) { my %result; - local $dbh->{RaiseError} = 1; - local $dbh->{PrintError} = 0; eval { my ($schema,$tab) = $table =~ /^(.+?)\.(.+)$/ ? ($1,$2) : (undef,$table); my $sth = $dbh->column_info( undef,$schema, $tab, '%' ); $sth->execute(); - while ( my $info = $sth->fetchrow_hashref() ){ my %column_info; $column_info{data_type} = $info->{TYPE_NAME}; @@ -903,17 +977,26 @@ sub columns_info_for { return \%result; } +sub columns_info_for { + my ($self, $table) = @_; + $self->dbh_do($self->can('_dbh_columns_info_for'), $table); +} + =head2 last_insert_id Return the row id of the last insert. =cut -sub last_insert_id { - my ($self, $row) = @_; - - return $self->dbh->func('last_insert_rowid'); +sub _dbh_last_insert_id { + my ($self, $dbh, $source, $col) = @_; + # XXX This is a SQLite-ism as a default... is there a DBI-generic way? + $dbh->func('last_insert_rowid'); +} +sub last_insert_id { + my $self = shift; + $self->dbh_do($self->can('_dbh_last_insert_id'), @_); } =head2 sqlt_type @@ -932,7 +1015,7 @@ sub sqlt_type { shift->dbh->{Driver}->{Name} } =back -Creates an SQL file based on the Schema, for each of the specified +Creates a SQL file based on the Schema, for each of the specified database types, in the given directory. Note that this feature is currently EXPERIMENTAL and may not work correctly @@ -992,8 +1075,24 @@ sub create_ddl_dir =head2 deployment_statements -Create the statements for L and -L. +=over 4 + +=item Arguments: $schema, $type, $version, $directory, $sqlt_args + +=back + +Returns the statements used by L and L. +The database driver name is given by C<$type>, though the value from +L is used if it is not specified. + +C<$directory> is used to return statements from files in a previously created +L directory and is optional. The filenames are constructed +from L, the schema name and the C<$version>. + +If no C<$directory> is specified then the statements are constructed on the +fly using L and C<$version> is ignored. + +See L for a list of values for C<$sqlt_args>. =cut @@ -1033,17 +1132,9 @@ sub deployment_statements { } -=head2 deploy - -Sends the appropriate statements to create or modify tables to the -db. This would normally be called through -L. - -=cut - sub deploy { - my ($self, $schema, $type, $sqltargs) = @_; - foreach my $statement ( $self->deployment_statements($schema, $type, undef, undef, { no_comments => 1, %{ $sqltargs || {} } } ) ) { + my ($self, $schema, $type, $sqltargs, $dir) = @_; + foreach my $statement ( $self->deployment_statements($schema, $type, undef, $dir, { no_comments => 1, %{ $sqltargs || {} } } ) ) { for ( split(";\n", $statement)) { next if($_ =~ /^--/); next if(!$_); @@ -1052,7 +1143,7 @@ sub deploy { next if($_ =~ /^COMMIT/m); next if $_ =~ /^\s+$/; # skip whitespace only $self->debugobj->query_start($_) if $self->debug; - $self->dbh->do($_) or warn "SQL was:\n $_"; + $self->dbh->do($_) or warn "SQL was:\n $_"; # XXX exceptions? $self->debugobj->query_end($_) if $self->debug; } } @@ -1092,7 +1183,12 @@ sub build_datetime_parser { return $type; } -sub DESTROY { shift->disconnect } +sub DESTROY { + my $self = shift; + return if !$self->_dbh; + $self->_verify_pid; + $self->_dbh(undef); +} 1; @@ -1131,25 +1227,6 @@ For setting, this method is deprecated in favor of L. =back -=head1 ENVIRONMENT VARIABLES - -=head2 DBIC_TRACE - -If C is set then SQL trace information -is produced (as when the L method is set). - -If the value is of the form C<1=/path/name> then the trace output is -written to the file C. - -This environment variable is checked when the storage object is first -created (when you call connect on your schema). So, run-time changes -to this environment variable will not take effect unless you also -re-connect on your schema. - -=head2 DBIX_CLASS_STORAGE_DBI_DEBUG - -Old name for DBIC_TRACE - =head1 AUTHORS Matt S. Trout @@ -1161,4 +1238,3 @@ Andy Grundman You may distribute this code under the same terms as Perl itself. =cut -