X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FDBIx%2FClass%2FStorage%2FDBI%2FCursor.pm;h=155855979a5f3a74602076526df19876a8e0bc41;hb=82c5f9168e30bc9dc7b681058298bb342582c5ec;hp=3ccaef971760a7f77a4bdc539f8684aa72f26189;hpb=1a14aa3f5016c456db28c148abab256ea72776df;p=dbsrgits%2FDBIx-Class.git diff --git a/lib/DBIx/Class/Storage/DBI/Cursor.pm b/lib/DBIx/Class/Storage/DBI/Cursor.pm index 3ccaef9..1558559 100644 --- a/lib/DBIx/Class/Storage/DBI/Cursor.pm +++ b/lib/DBIx/Class/Storage/DBI/Cursor.pm @@ -1,60 +1,301 @@ package DBIx::Class::Storage::DBI::Cursor; -use base qw/DBIx::Class::Cursor/; - use strict; use warnings; -sub new { - my ($it_class, $sth, $args, $attrs) = @_; - #use Data::Dumper; warn Dumper(@_); - $it_class = ref $it_class if ref $it_class; - my $new = { - sth => $sth, - args => $args, - pos => 0, - attrs => $attrs }; - return bless ($new, $it_class); +use base 'DBIx::Class::Cursor'; + +use Scalar::Util qw(refaddr weaken); +use DBIx::Class::_Util qw( detected_reinvoked_destructor dbic_internal_try ); +use namespace::clean; + +__PACKAGE__->mk_group_accessors('simple' => + qw/storage args attrs/ +); + +=head1 NAME + +DBIx::Class::Storage::DBI::Cursor - Object representing a query cursor on a +resultset. + +=head1 SYNOPSIS + + my $cursor = $schema->resultset('CD')->cursor(); + + # raw values off the database handle in resultset columns/select order + my @next_cd_column_values = $cursor->next; + + # list of all raw values as arrayrefs + my @all_cds_column_values = $cursor->all; + +=head1 DESCRIPTION + +A Cursor represents a query cursor on a L object. It +allows for traversing the result set with L, retrieving all results with +L and resetting the cursor with L. + +Usually, you would use the cursor methods built into L +to traverse it. See L, +L and L for more +information. + +=head1 METHODS + +=head2 new + +Returns a new L object. + +=cut + +{ + my %cursor_registry; + + sub new { + my ($class, $storage, $args, $attrs) = @_; + + my $self = bless { + storage => $storage, + args => $args, + attrs => $attrs, + }, ref $class || $class; + + if (DBIx::Class::_ENV_::HAS_ITHREADS) { + + # quick "garbage collection" pass - prevents the registry + # from slowly growing with a bunch of undef-valued keys + defined $cursor_registry{$_} or delete $cursor_registry{$_} + for keys %cursor_registry; + + weaken( $cursor_registry{ refaddr($self) } = $self ) + } + + return $self; + } + + sub DBIx::Class::__DBI_Cursor_iThreads_handler__::CLONE { + for (keys %cursor_registry) { + # once marked we no longer care about them, hence no + # need to keep in the registry, left alone renumber the + # keys (all addresses are now different) + my $self = delete $cursor_registry{$_} + or next; + + $self->{_intra_thread} = 1; + } + + # Dummy NEXTSTATE ensuring the all temporaries on the stack are garbage + # collected before leaving this scope. Depending on the code above, this + # may very well be just a preventive measure guarding future modifications + undef; + } } +=head2 next + +=over 4 + +=item Arguments: none + +=item Return Value: \@row_columns + +=back + +Advances the cursor to the next row and returns an array of column +values (the result of L method). + +=cut + sub next { - my ($self) = @_; - return if $self->{attrs}{rows} - && $self->{pos} >= $self->{attrs}{rows}; # + $self->{attrs}{offset}); - my $sth = $self->{sth}; - unless ($self->{live_sth}) { - $sth->execute(@{$self->{args} || []}); - if (my $offset = $self->{attrs}{offset}) { - $sth->fetch for 1 .. $offset; - } - $self->{live_sth} = 1; - } - my @row = $sth->fetchrow_array; - $self->{pos}++ if @row; - return @row; + my $self = shift; + + return if $self->{_done}; + + my $sth; + + if ( + $self->{attrs}{software_limit} + && $self->{attrs}{rows} + && ($self->{_pos}||0) >= $self->{attrs}{rows} + ) { + if ($sth = $self->sth) { + # explicit finish will issue warnings, unlike the DESTROY below + $sth->finish if $sth->FETCH('Active'); + } + $self->{_done} = 1; + return; + } + + unless ($sth = $self->sth) { + (undef, $sth, undef) = $self->storage->_select( @{$self->{args}} ); + + $self->{_results} = [ (undef) x $sth->FETCH('NUM_OF_FIELDS') ]; + $sth->bind_columns( \( @{$self->{_results}} ) ); + + if ( $self->{attrs}{software_limit} and $self->{attrs}{offset} ) { + $sth->fetch for 1 .. $self->{attrs}{offset}; + } + + $self->sth($sth); + } + + if ($sth->fetch) { + $self->{_pos}++; + return @{$self->{_results}}; + } else { + $self->{_done} = 1; + return (); + } } + +=head2 all + +=over 4 + +=item Arguments: none + +=item Return Value: \@row_columns+ + +=back + +Returns a list of arrayrefs of column values for all rows in the +L. + +=cut + sub all { - my ($self) = @_; - return $self->SUPER::all if $self->{attrs}{rows}; - my $sth = $self->{sth}; - $sth->finish if $sth->{Active}; - $sth->execute(@{$self->{args} || []}); - delete $self->{live_sth}; - return @{$sth->fetchall_arrayref}; + my $self = shift; + + # delegate to DBIC::Cursor which will delegate back to next() + if ($self->{attrs}{software_limit} + && ($self->{attrs}{offset} || $self->{attrs}{rows})) { + return $self->next::method(@_); + } + + my $sth; + + if ($sth = $self->sth) { + # explicit finish will issue warnings, unlike the DESTROY below + $sth->finish if ( ! $self->{_done} and $sth->FETCH('Active') ); + $self->sth(undef); + } + + (undef, $sth) = $self->storage->_select( @{$self->{args}} ); + + ( + DBIx::Class::_ENV_::SHUFFLE_UNORDERED_RESULTSETS + and + ! $self->{attrs}{order_by} + and + require List::Util + ) + ? List::Util::shuffle( @{$sth->fetchall_arrayref} ) + : @{$sth->fetchall_arrayref} + ; +} + +sub sth { + my $self = shift; + + if (@_) { + delete @{$self}{qw/_pos _done _pid _intra_thread/}; + + $self->{sth} = $_[0]; + $self->{_pid} = $$ if ! DBIx::Class::_ENV_::BROKEN_FORK and $_[0]; + } + elsif ($self->{sth} and ! $self->{_done}) { + + my $invalidate_handle_reason; + + if (DBIx::Class::_ENV_::HAS_ITHREADS and $self->{_intra_thread} ) { + $invalidate_handle_reason = 'Multi-thread'; + } + elsif (!DBIx::Class::_ENV_::BROKEN_FORK and $self->{_pid} != $$ ) { + $invalidate_handle_reason = 'Multi-process'; + } + + if ($invalidate_handle_reason) { + $self->storage->throw_exception("$invalidate_handle_reason access attempted while cursor in progress (position $self->{_pos})") + if $self->{_pos}; + + # reinvokes the reset logic above + $self->sth(undef); + } + } + + return $self->{sth}; } +=head2 reset + +Resets the cursor to the beginning of the L. + +=cut + sub reset { - my ($self) = @_; - $self->{sth}->finish if $self->{sth}->{Active}; - $self->{pos} = 0; - $self->{live_sth} = 0; - return $self; + $_[0]->__finish_sth if $_[0]->{sth}; + $_[0]->sth(undef); + + # Dummy NEXTSTATE ensuring the all temporaries on the stack are garbage + # collected before leaving this scope. Depending on the code above, this + # may very well be just a preventive measure guarding future modifications + undef; } + sub DESTROY { - my ($self) = @_; - $self->{sth}->finish if $self->{sth}->{Active}; + return if &detected_reinvoked_destructor; + + $_[0]->__finish_sth if $_[0]->{sth}; + + # Dummy NEXTSTATE ensuring the all temporaries on the stack are garbage + # collected before leaving this scope. Depending on the code above, this + # may very well be just a preventive measure guarding future modifications + undef; +} + +sub __finish_sth { + # It is (sadly) extremely important to finish() handles we are about + # to lose (due to reset() or a DESTROY() ). $rs->reset is the closest + # thing the user has to getting to the underlying finish() API and some + # DBDs mandate this (e.g. DBD::InterBase will segfault, DBD::Sybase + # won't start a transaction sanely, etc) + # We also can't use the accessor here, as it will trigger a fork/thread + # check, and resetting a cursor in a child is perfectly valid + + my $self = shift; + + # No need to care about failures here + dbic_internal_try { + local $SIG{__WARN__} = sub {}; + $self->{sth}->finish + } if ( + $self->{sth} + and + # weird double-negative to catch the case of ->FETCH throwing + # and attempt a finish *anyway* + ! dbic_internal_try { + ! $self->{sth}->FETCH('Active') + } + ); + + # Dummy NEXTSTATE ensuring the all temporaries on the stack are garbage + # collected before leaving this scope. Depending on the code above, this + # may very well be just a preventive measure guarding future modifications + undef; } +=head1 FURTHER QUESTIONS? + +Check the list of L. + +=head1 COPYRIGHT AND LICENSE + +This module is free software L +by the L. You can +redistribute it and/or modify it under the same terms as the +L. + +=cut + 1;