X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FDBIx%2FClass%2FResultSet.pm;h=a216589f68166e72caf90d62744251a4d5dc7278;hb=25594f038dc8bb8d8c3f110ccfbebe30774043be;hp=6178af7e577dfc67223d7c96be7f5fbd4f206539;hpb=ee38fa40d848a164040097286b4440d4b64565e8;p=dbsrgits%2FDBIx-Class-Historic.git diff --git a/lib/DBIx/Class/ResultSet.pm b/lib/DBIx/Class/ResultSet.pm index 6178af7..a216589 100644 --- a/lib/DBIx/Class/ResultSet.pm +++ b/lib/DBIx/Class/ResultSet.pm @@ -3,308 +3,2124 @@ package DBIx::Class::ResultSet; use strict; use warnings; use overload - '0+' => 'count', + '0+' => \&count, + 'bool' => sub { 1; }, fallback => 1; +use Carp::Clan qw/^DBIx::Class/; use Data::Page; +use Storable; +use Data::Dumper; +use Scalar::Util qw/weaken/; +use Data::Dumper; +use DBIx::Class::ResultSetColumn; +use base qw/DBIx::Class/; +__PACKAGE__->load_components(qw/AccessorGroup/); +__PACKAGE__->mk_group_accessors('simple' => qw/result_source result_class/); =head1 NAME -DBIX::Class::Recordset - Responsible for fetching and creating recordsets. +DBIx::Class::ResultSet - Responsible for fetching and creating resultset. -=head1 SYNOPSIS; +=head1 SYNOPSIS -$rs=MyApp::DB::Class->search(registered=>1); + my $rs = $schema->resultset('User')->search(registered => 1); + my @rows = $schema->resultset('CD')->search(year => 2005); =head1 DESCRIPTION -The recordset is also know as an iterator. +The resultset is also known as an iterator. It is responsible for handling +queries that may return an arbitrary number of rows, e.g. via L +or a C relationship. + +In the examples below, the following table classes are used: + + package MyApp::Schema::Artist; + use base qw/DBIx::Class/; + __PACKAGE__->load_components(qw/Core/); + __PACKAGE__->table('artist'); + __PACKAGE__->add_columns(qw/artistid name/); + __PACKAGE__->set_primary_key('artistid'); + __PACKAGE__->has_many(cds => 'MyApp::Schema::CD'); + 1; + + package MyApp::Schema::CD; + use base qw/DBIx::Class/; + __PACKAGE__->load_components(qw/Core/); + __PACKAGE__->table('cd'); + __PACKAGE__->add_columns(qw/cdid artist title year/); + __PACKAGE__->set_primary_key('cdid'); + __PACKAGE__->belongs_to(artist => 'MyApp::Schema::Artist'); + 1; =head1 METHODS +=head2 new + =over 4 -=item new +=item Arguments: $source, \%$attrs + +=item Return Value: $rs + +=back + +The resultset constructor. Takes a source object (usually a +L) and an attribute hash (see +L below). Does not perform any queries -- these are +executed as needed by the other methods. + +Generally you won't need to construct a resultset manually. You'll +automatically get one from e.g. a L called in scalar context: + + my $rs = $schema->resultset('CD')->search({ title => '100th Window' }); + +IMPORTANT: If called on an object, proxies to new_result instead so -The recordset constructor. Takes a db class and an -attribute hash (see below for more info on attributes) + my $cd = $schema->resultset('CD')->new({ title => 'Spoon' }); + +will return a CD object, not a ResultSet. =cut sub new { - my ($it_class, $db_class, $attrs) = @_; - #use Data::Dumper; warn Dumper(@_); - $it_class = ref $it_class if ref $it_class; - $attrs = { %{ $attrs || {} } }; - my %seen; - $attrs->{cols} ||= [ map { "me.$_" } $db_class->_select_columns ]; - $attrs->{from} ||= [ { 'me' => $db_class->_table_name } ]; - if ($attrs->{join}) { - foreach my $j (ref $attrs->{join} eq 'ARRAY' - ? (@{$attrs->{join}}) : ($attrs->{join})) { - if (ref $j eq 'HASH') { - $seen{$_} = 1 foreach keys %$j; - } else { - $seen{$j} = 1; - } - } - push(@{$attrs->{from}}, $db_class->_resolve_join($attrs->{join}, 'me')); - } - foreach my $pre (@{$attrs->{prefetch} || []}) { - push(@{$attrs->{from}}, $db_class->_resolve_join($pre, 'me')) - unless $seen{$pre}; - push(@{$attrs->{cols}}, - map { "$pre.$_" } - $db_class->_relationships->{$pre}->{class}->_select_columns); - } - my $new = { - class => $db_class, - cols => $attrs->{cols} || [ $db_class->_select_columns ], + my $class = shift; + return $class->new_result(@_) if ref $class; + + my ($source, $attrs) = @_; + weaken $source; + + if ($attrs->{page}) { + $attrs->{rows} ||= 10; + $attrs->{offset} ||= 0; + $attrs->{offset} += ($attrs->{rows} * ($attrs->{page} - 1)); + } + + $attrs->{alias} ||= 'me'; + + bless { + result_source => $source, + result_class => $attrs->{result_class} || $source->result_class, cond => $attrs->{where}, - from => $attrs->{from} || $db_class->_table_name, +# from => $attrs->{from}, +# collapse => $collapse, count => undef, + page => delete $attrs->{page}, pager => undef, - attrs => $attrs }; - bless ($new, $it_class); - $new->pager if ($attrs->{page}); - return $new; + attrs => $attrs + }, $class; } -=item cursor +=head2 search -Return a storage driven cursor to the given record set. +=over 4 -=cut +=item Arguments: $cond, \%attrs? -sub cursor { - my ($self) = @_; - my ($db_class, $attrs) = @{$self}{qw/class attrs/}; - if ($attrs->{page}) { - $attrs->{rows} = $self->pager->entries_per_page; - $attrs->{offset} = $self->pager->skipped; - } - return $self->{cursor} - ||= $db_class->storage->select($self->{from}, $self->{cols}, - $attrs->{where},$attrs); -} +=item Return Value: $resultset (scalar context), @row_objs (list context) + +=back + + my @cds = $cd_rs->search({ year => 2001 }); # "... WHERE year = 2001" + my $new_rs = $cd_rs->search({ year => 2005 }); -=item slice + my $new_rs = $cd_rs->search([ { year => 2005 }, { year => 2004 } ]); + # year = 2005 OR year = 2004 -return a number of elements from the given record set. +If you need to pass in additional attributes but no additional condition, +call it as C. + + # "SELECT name, artistid FROM $artist_table" + my @all_artists = $schema->resultset('Artist')->search(undef, { + columns => [qw/name artistid/], + }); =cut -sub slice { - my ($self, $min, $max) = @_; - my $attrs = { %{ $self->{attrs} || {} } }; - $self->{class}->throw("Can't slice without where") unless $attrs->{where}; - $attrs->{offset} = $min; - $attrs->{rows} = ($max ? ($max - $min + 1) : 1); - my $slice = $self->new($self->{class}, $attrs); - return (wantarray ? $slice->all : $slice); +sub search { + my $self = shift; + my $rs = $self->search_rs( @_ ); + return (wantarray ? $rs->all : $rs); } -=item next +=head2 search_rs + +=over 4 + +=item Arguments: $cond, \%attrs? + +=item Return Value: $resultset + +=back -Returns the next element in this record set. +This method does the same exact thing as search() except it will +always return a resultset, even in list context. =cut -sub next { - my ($self) = @_; - my @row = $self->cursor->next; - return unless (@row); - return $self->_construct_object(@row); -} +sub search_rs { + my $self = shift; + + my $attrs = {}; + $attrs = pop(@_) if @_ > 1 and ref $_[$#_] eq 'HASH'; + my $our_attrs = ($attrs->{_parent_attrs}) + ? { %{$attrs->{_parent_attrs}} } + : { %{$self->{attrs}} }; + my $having = delete $our_attrs->{having}; + + # XXX this is getting messy + if ($attrs->{_live_join_stack}) { + my $live_join = $attrs->{_live_join_stack}; + foreach (reverse @{$live_join}) { + $attrs->{_live_join_h} = (defined $attrs->{_live_join_h}) ? { $_ => $attrs->{_live_join_h} } : $_; + } + } -sub _construct_object { - my ($self, @row) = @_; - my @cols = $self->{class}->_select_columns; - my $new; - unless ($self->{attrs}{prefetch}) { - $new = $self->{class}->_row_to_object(\@cols, \@row); - } else { - my @main = splice(@row, 0, scalar @cols); - $new = $self->{class}->_row_to_object(\@cols, \@main); - PRE: foreach my $pre (@{$self->{attrs}{prefetch}}) { - my $rel_obj = $self->{class}->_relationships->{$pre}; - my $pre_class = $self->{class}->resolve_class($rel_obj->{class}); - my @pre_cols = $pre_class->_select_columns; - my @vals = splice(@row, 0, scalar @pre_cols); - my $fetched = $pre_class->_row_to_object(\@pre_cols, \@vals); - $self->{class}->throw("No accessor for prefetched $pre") - unless defined $rel_obj->{attrs}{accessor}; - if ($rel_obj->{attrs}{accessor} eq 'single') { - foreach my $pri ($rel_obj->{class}->primary_columns) { - next PRE unless defined $fetched->get_column($pri); - } - $new->{_relationship_data}{$pre} = $fetched; - } elsif ($rel_obj->{attrs}{accessor} eq 'filter') { - $new->{_inflated_column}{$pre} = $fetched; - } else { - $self->{class}->throw("Don't know how to store prefetched $pre"); + # merge new attrs into old + foreach my $key (qw/join prefetch/) { + next unless (exists $attrs->{$key}); + if ($attrs->{_live_join_stack} || $our_attrs->{_live_join_stack}) { + my $live_join = $attrs->{_live_join_stack} || + $our_attrs->{_live_join_stack}; + foreach (reverse @{$live_join}) { + $attrs->{$key} = { $_ => $attrs->{$key} }; } } + + if (exists $our_attrs->{$key}) { + $our_attrs->{$key} = $self->_merge_attr($our_attrs->{$key}, $attrs->{$key}); + } else { + $our_attrs->{$key} = $attrs->{$key}; + } + delete $attrs->{$key}; } - $new = $self->{attrs}{record_filter}->($new) - if exists $self->{attrs}{record_filter}; - return $new; -} -=item count + $our_attrs->{join} = $self->_merge_attr( + $our_attrs->{join}, $attrs->{_live_join_h}, 1 + ) if ($attrs->{_live_join_h}); -Performs an SQL count with the same query as the resultset was built -with to find the number of elements. + if (defined $our_attrs->{prefetch}) { + $our_attrs->{join} = $self->_merge_attr( + $our_attrs->{join}, $our_attrs->{prefetch}, 1 + ); + } -=cut + my $new_attrs = { %{$our_attrs}, %{$attrs} }; + my $where = (@_ + ? ( + (@_ == 1 || ref $_[0] eq "HASH") + ? shift + : ( + (@_ % 2) + ? $self->throw_exception("Odd number of arguments to search") + : {@_} + ) + ) + : undef() + ); + + if (defined $where) { + $new_attrs->{where} = ( + defined $new_attrs->{where} + ? { '-and' => [ + map { + ref $_ eq 'ARRAY' ? [ -or => $_ ] : $_ + } $where, $new_attrs->{where} + ] + } + : $where); + } + + if (defined $having) { + $new_attrs->{having} = ( + defined $new_attrs->{having} + ? { '-and' => [ + map { + ref $_ eq 'ARRAY' ? [ -or => $_ ] : $_ + } $having, $new_attrs->{having} + ] + } + : $having); + } + my $rs = (ref $self)->new($self->result_source, $new_attrs); + $rs->{_parent_rs} = $self->{_parent_rs} if ($self->{_parent_rs}); + #XXX - hack to pass through parent of related resultsets -sub count { - my ($self) = @_; - my $db_class = $self->{class}; - my $attrs = { %{ $self->{attrs} } }; - unless ($self->{count}) { - # offset and order by are not needed to count - delete $attrs->{$_} for qw/offset order_by/; - - my @cols = 'COUNT(*)'; - $self->{count} = $db_class->storage->select_single($self->{from}, \@cols, - $self->{cond}, $attrs); + unless (@_) { # no search, effectively just a clone + my $rows = $self->get_cache; + if ($rows) { + $rs->set_cache($rows); + } } - return 0 unless $self->{count}; - return $self->{pager}->entries_on_this_page if ($self->{pager}); - return ( $attrs->{rows} && $attrs->{rows} < $self->{count} ) - ? $attrs->{rows} - : $self->{count}; + + return $rs; } -=item all +=head2 search_literal + +=over 4 + +=item Arguments: $sql_fragment, @bind_values + +=item Return Value: $resultset (scalar context), @row_objs (list context) -Returns all elements in the recordset. Is called implictly if the search -method is used in list context. +=back + + my @cds = $cd_rs->search_literal('year = ? AND title = ?', qw/2001 Reload/); + my $newrs = $artist_rs->search_literal('name = ?', 'Metallica'); + +Pass a literal chunk of SQL to be added to the conditional part of the +resultset query. =cut -sub all { - my ($self) = @_; - return map { $self->_construct_object(@$_); } - $self->cursor->all; +sub search_literal { + my ($self, $cond, @vals) = @_; + my $attrs = (ref $vals[$#vals] eq 'HASH' ? { %{ pop(@vals) } } : {}); + $attrs->{bind} = [ @{$self->{attrs}{bind}||[]}, @vals ]; + return $self->search(\$cond, $attrs); } -=item reset +=head2 find + +=over 4 + +=item Arguments: @values | \%cols, \%attrs? -Reset this recordset's cursor, so you can iterate through the elements again. +=item Return Value: $row_object + +=back + +Finds a row based on its primary key or unique constraint. For example, to find +a row by its primary key: + + my $cd = $schema->resultset('CD')->find(5); + +You can also find a row by a specific unique constraint using the C +attribute. For example: + + my $cd = $schema->resultset('CD')->find('Massive Attack', 'Mezzanine', { + key => 'cd_artist_title' + }); + +Additionally, you can specify the columns explicitly by name: + + my $cd = $schema->resultset('CD')->find( + { + artist => 'Massive Attack', + title => 'Mezzanine', + }, + { key => 'cd_artist_title' } + ); + +If the C is specified as C, it searches only on the primary key. + +If no C is specified, it searches on all unique constraints defined on the +source, including the primary key. + +See also L and L. For information on how to +declare unique constraints, see +L. =cut -sub reset { - my ($self) = @_; - $self->cursor->reset; - return $self; +sub find { + my $self = shift; + my $attrs = (@_ > 1 && ref $_[$#_] eq 'HASH' ? pop(@_) : {}); + + # Default to the primary key, but allow a specific key + my @cols = exists $attrs->{key} + ? $self->result_source->unique_constraint_columns($attrs->{key}) + : $self->result_source->primary_columns; + $self->throw_exception( + "Can't find unless a primary key or unique constraint is defined" + ) unless @cols; + + # Parse out a hashref from input + my $input_query; + if (ref $_[0] eq 'HASH') { + $input_query = { %{$_[0]} }; + } + elsif (@_ == @cols) { + $input_query = {}; + @{$input_query}{@cols} = @_; + } + else { + # Compatibility: Allow e.g. find(id => $value) + carp "Find by key => value deprecated; please use a hashref instead"; + $input_query = {@_}; + } + + my @unique_queries = $self->_unique_queries($input_query, $attrs); + + # Handle cases where the ResultSet defines the query, or where the user is + # abusing find + my $query = @unique_queries ? \@unique_queries : $input_query; + + # Run the query + if (keys %$attrs) { + my $rs = $self->search($query, $attrs); + $rs->_resolve; + return keys %{$rs->{_attrs}->{collapse}} ? $rs->next : $rs->single; + } + else { + $self->_resolve; + return (keys %{$self->{_attrs}->{collapse}}) + ? $self->search($query)->next + : $self->single($query); + } } -=item first +# _unique_queries +# +# Build a list of queries which satisfy unique constraints. -resets the recordset and returns the first element. +sub _unique_queries { + my ($self, $query, $attrs) = @_; -=cut + my @constraint_names = exists $attrs->{key} + ? ($attrs->{key}) + : $self->result_source->unique_constraint_names; -sub first { - return $_[0]->reset->next; + my @unique_queries; + foreach my $name (@constraint_names) { + my @unique_cols = $self->result_source->unique_constraint_columns($name); + my $unique_query = $self->_build_unique_query($query, \@unique_cols); + + next unless scalar keys %$unique_query; + + # Add the ResultSet's alias + foreach my $key (grep { ! m/\./ } keys %$unique_query) { + my $alias = ($self->{attrs}->{_live_join}) + ? $self->{attrs}->{_live_join} + : $self->{attrs}->{alias}; + $unique_query->{"$alias.$key"} = delete $unique_query->{$key}; + } + + push @unique_queries, $unique_query; + } + + return @unique_queries; } -=item delete +# _build_unique_query +# +# Constrain the specified query hash based on the specified column names. -Deletes all elements in the recordset. +sub _build_unique_query { + my ($self, $query, $unique_cols) = @_; + + my %unique_query = + map { $_ => $query->{$_} } + grep { exists $query->{$_} } + @$unique_cols; + + return \%unique_query; +} + +=head2 search_related + +=over 4 + +=item Arguments: $rel, $cond, \%attrs? + +=item Return Value: $new_resultset + +=back + + $new_rs = $cd_rs->search_related('artist', { + name => 'Emo-R-Us', + }); + +Searches the specified relationship, optionally specifying a condition and +attributes for matching records. See L for more information. =cut -sub delete { - my ($self) = @_; - $_->delete for $self->all; - return 1; +sub search_related { + return shift->related_resultset(shift)->search(@_); } -*delete_all = \&delete; # Yeah, yeah, yeah ... +=head2 cursor -=item pager +=over 4 + +=item Arguments: none + +=item Return Value: $cursor + +=back -Returns a L object for the current resultset. Only makes -sense for queries with page turned on. +Returns a storage-driven cursor to the given resultset. See +L for more information. =cut -sub pager { +sub cursor { my ($self) = @_; - my $attrs = $self->{attrs}; - delete $attrs->{offset}; - my $rows_per_page = delete $attrs->{rows} || 10; - $self->{pager} ||= Data::Page->new( - $self->count, $rows_per_page, $attrs->{page} || 1); - $attrs->{rows} = $rows_per_page; - return $self->{pager}; + + $self->_resolve; + my $attrs = { %{$self->{_attrs}} }; + return $self->{cursor} + ||= $self->result_source->storage->select($attrs->{from}, $attrs->{select}, + $attrs->{where},$attrs); } -=item page +=head2 single + +=over 4 + +=item Arguments: $cond? + +=item Return Value: $row_object? + +=back + + my $cd = $schema->resultset('CD')->single({ year => 2001 }); -Returns a new recordset representing a given page. +Inflates the first result without creating a cursor if the resultset has +any records in it; if not returns nothing. Used by L as an optimisation. + +Can optionally take an additional condition *only* - this is a fast-code-path +method; if you need to add extra joins or similar call ->search and then +->single without a condition on the $rs returned from that. =cut -sub page { - my ($self, $page) = @_; - my $attrs = $self->{attrs}; - $attrs->{page} = $page; - return $self->new($self->{class}, $attrs); +sub single { + my ($self, $where) = @_; + $self->_resolve; + my $attrs = { %{$self->{_attrs}} }; + if ($where) { + if (defined $attrs->{where}) { + $attrs->{where} = { + '-and' => + [ map { ref $_ eq 'ARRAY' ? [ -or => $_ ] : $_ } + $where, delete $attrs->{where} ] + }; + } else { + $attrs->{where} = $where; + } + } + + unless ($self->_is_unique_query($attrs->{where})) { + carp "Query not guaranteed to return a single row" + . "; please declare your unique constraints or use search instead"; + } + + my @data = $self->result_source->storage->select_single( + $attrs->{from}, $attrs->{select}, + $attrs->{where},$attrs + ); + + return (@data ? $self->_construct_object(@data) : ()); +} + +# _is_unique_query +# +# Try to determine if the specified query is guaranteed to be unique, based on +# the declared unique constraints. + +sub _is_unique_query { + my ($self, $query) = @_; + + my $collapsed = $self->_collapse_query($query); + my $alias = ($self->{attrs}->{_live_join}) + ? $self->{attrs}->{_live_join} + : $self->{attrs}->{alias}; + + foreach my $name ($self->result_source->unique_constraint_names) { + my @unique_cols = map { + "$alias.$_" + } $self->result_source->unique_constraint_columns($name); + + # Count the values for each unique column + my %seen = map { $_ => 0 } @unique_cols; + + foreach my $key (keys %$collapsed) { + my $aliased = $key; + $aliased = "$alias.$key" unless $key =~ /\./; + + next unless exists $seen{$aliased}; # Additional constraints are okay + $seen{$aliased} = scalar @{ $collapsed->{$key} }; + } + + # If we get 0 or more than 1 value for a column, it's not necessarily unique + return 1 unless grep { $_ != 1 } values %seen; + } + + return 0; +} + +# _collapse_query +# +# Recursively collapse the query, accumulating values for each column. + +sub _collapse_query { + my ($self, $query, $collapsed) = @_; + + $collapsed ||= {}; + + if (ref $query eq 'ARRAY') { + foreach my $subquery (@$query) { + next unless ref $subquery; # -or +# warn "ARRAY: " . Dumper $subquery; + $collapsed = $self->_collapse_query($subquery, $collapsed); + } + } + elsif (ref $query eq 'HASH') { + if (keys %$query and (keys %$query)[0] eq '-and') { + foreach my $subquery (@{$query->{-and}}) { +# warn "HASH: " . Dumper $subquery; + $collapsed = $self->_collapse_query($subquery, $collapsed); + } + } + else { +# warn "LEAF: " . Dumper $query; + foreach my $key (keys %$query) { + push @{$collapsed->{$key}}, $query->{$key}; + } + } + } + + return $collapsed; } -=back +=head2 get_column + +=over 4 + +=item Arguments: $cond? + +=item Return Value: $resultsetcolumn + +=back + + my $max_length = $rs->get_column('length')->max; + +Returns a ResultSetColumn instance for $column based on $self + +=cut -=head1 Attributes +sub get_column { + my ($self, $column) = @_; + my $new = DBIx::Class::ResultSetColumn->new($self, $column); + return $new; +} -The recordset is responsible for handling the various attributes that -can be passed in with the search functions. Here's an overview of them: +=head2 search_like =over 4 -=item order_by +=item Arguments: $cond, \%attrs? + +=item Return Value: $resultset (scalar context), @row_objs (list context) + +=back + + # WHERE title LIKE '%blue%' + $cd_rs = $rs->search_like({ title => '%blue%'}); -Which column to order the results by. +Performs a search, but uses C instead of C<=> as the condition. Note +that this is simply a convenience method. You most likely want to use +L with specific operators. -=item cols +For more information, see L. + +=cut -Which cols should be retrieved on the first search. +sub search_like { + my $class = shift; + my $attrs = (@_ > 1 && ref $_[$#_] eq 'HASH' ? pop(@_) : {}); + my $query = ref $_[0] eq 'HASH' ? { %{shift()} }: {@_}; + $query->{$_} = { 'like' => $query->{$_} } for keys %$query; + return $class->search($query, { %$attrs }); +} -=item join +=head2 slice -Contains a list of relations that should be joined for this query. Can also -contain a hash referece to refer to that relation's relations. +=over 4 -=item from +=item Arguments: $first, $last -This attribute can contain a arrayref of elements. each element can be another -arrayref, to nest joins, or it can be a hash which represents the two sides -of the join. +=item Return Value: $resultset (scalar context), @row_objs (list context) -*NOTE* Use this on your own risk. This allows you to shoot your foot off! +=back -=item page +Returns a resultset or object list representing a subset of elements from the +resultset slice is called on. Indexes are from 0, i.e., to get the first +three records, call: -Should the resultset be paged? This can also be enabled by using the -'page' option. + my ($one, $two, $three) = $rs->slice(0, 2); -=item rows +=cut -For paged resultsset, how many rows per page +sub slice { + my ($self, $min, $max) = @_; + my $attrs = {}; # = { %{ $self->{attrs} || {} } }; + $attrs->{offset} = $self->{attrs}{offset} || 0; + $attrs->{offset} += $min; + $attrs->{rows} = ($max ? ($max - $min + 1) : 1); + return $self->search(undef(), $attrs); + #my $slice = (ref $self)->new($self->result_source, $attrs); + #return (wantarray ? $slice->all : $slice); +} -=item offset +=head2 next -For paged resultsset, which page to start on. +=over 4 -=item accesor +=item Arguments: none -Tells the recordset how to prefetch relations. Can either be 'single' or -'filter'. +=item Return Value: $result? =back +Returns the next element in the resultset (C is there is none). + +Can be used to efficiently iterate over records in the resultset: + + my $rs = $schema->resultset('CD')->search; + while (my $cd = $rs->next) { + print $cd->title; + } + +Note that you need to store the resultset object, and call C on it. +Calling C<< resultset('Table')->next >> repeatedly will always return the +first record from the resultset. + +=cut + +sub next { + my ($self) = @_; + if (my $cache = $self->get_cache) { + $self->{all_cache_position} ||= 0; + return $cache->[$self->{all_cache_position}++]; + } + if ($self->{attrs}{cache}) { + $self->{all_cache_position} = 1; + return ($self->all)[0]; + } + my @row = ( + exists $self->{stashed_row} + ? @{delete $self->{stashed_row}} + : $self->cursor->next + ); + return unless (@row); + return $self->_construct_object(@row); +} + +sub _resolve { + my $self = shift; + + return if(exists $self->{_attrs}); #return if _resolve has already been called + + my $attrs = $self->{attrs}; + my $source = ($self->{_parent_rs}) + ? $self->{_parent_rs} + : $self->{result_source}; + + # XXX - lose storable dclone + my $record_filter = delete $attrs->{record_filter} + if (defined $attrs->{record_filter}); + $attrs = Storable::dclone($attrs || {}); # { %{ $attrs || {} } }; + $attrs->{record_filter} = $record_filter if ($record_filter); + $self->{attrs}->{record_filter} = $record_filter if ($record_filter); + + my $alias = $attrs->{alias}; + + $attrs->{columns} ||= delete $attrs->{cols} if $attrs->{cols}; + delete $attrs->{as} if $attrs->{columns}; + $attrs->{columns} ||= [ $self->{result_source}->columns ] + unless $attrs->{select}; + my $select_alias = ($self->{_parent_rs}) + ? $self->{attrs}->{_live_join} + : $alias; + $attrs->{select} = [ + map { m/\./ ? $_ : "${select_alias}.$_" } @{delete $attrs->{columns}} + ] if $attrs->{columns}; + $attrs->{as} ||= [ + map { m/^\Q$alias.\E(.+)$/ ? $1 : $_ } @{$attrs->{select}} + ]; + if (my $include = delete $attrs->{include_columns}) { + push(@{$attrs->{select}}, @$include); + push(@{$attrs->{as}}, map { m/([^.]+)$/; $1; } @$include); + } + + $attrs->{from} ||= [ { $alias => $source->from } ]; + $attrs->{seen_join} ||= {}; + my %seen; + if (my $join = delete $attrs->{join}) { + foreach my $j (ref $join eq 'ARRAY' ? @$join : ($join)) { + if (ref $j eq 'HASH') { + $seen{$_} = 1 foreach keys %$j; + } else { + $seen{$j} = 1; + } + } + push(@{$attrs->{from}}, + $source->resolve_join($join, $attrs->{alias}, $attrs->{seen_join}) + ); + } + $attrs->{group_by} ||= $attrs->{select} if delete $attrs->{distinct}; + $attrs->{order_by} = [ $attrs->{order_by} ] if + $attrs->{order_by} and !ref($attrs->{order_by}); + $attrs->{order_by} ||= []; + + if(my $seladds = delete($attrs->{'+select'})) { + my @seladds = (ref($seladds) eq 'ARRAY' ? @$seladds : ($seladds)); + $attrs->{select} = [ + @{ $attrs->{select} }, + map { (m/\./ || ref($_)) ? $_ : "${alias}.$_" } $seladds + ]; + } + if(my $asadds = delete($attrs->{'+as'})) { + my @asadds = (ref($asadds) eq 'ARRAY' ? @$asadds : ($asadds)); + $attrs->{as} = [ @{ $attrs->{as} }, @asadds ]; + } + my $collapse = $attrs->{collapse} || {}; + if (my $prefetch = delete $attrs->{prefetch}) { + my @pre_order; + foreach my $p (ref $prefetch eq 'ARRAY' ? @$prefetch : ($prefetch)) { + if ( ref $p eq 'HASH' ) { + foreach my $key (keys %$p) { + push(@{$attrs->{from}}, $source->resolve_join($p, $attrs->{alias})) + unless $seen{$key}; + } + } else { + push(@{$attrs->{from}}, $source->resolve_join($p, $attrs->{alias})) + unless $seen{$p}; + } + + # we're about to resolve_join on the current class, so we need to bring + # the joins (which are from the original class) to the right level + # XXX the below alg is ridiculous + if ($attrs->{_live_join_stack}) { + STACK: + foreach (@{$attrs->{_live_join_stack}}) { + if (ref $p eq 'HASH') { + if (exists $p->{$_}) { + $p = $p->{$_}; + } else { + $p = undef; + last STACK; + } + } elsif (ref $p eq 'ARRAY') { + foreach my $pe (@{$p}) { + if ($pe eq $_) { + $p = undef; + last STACK; + } + next unless(ref $pe eq 'HASH'); + next unless(exists $pe->{$_}); + $p = $pe->{$_}; + next STACK; + } + $p = undef; + last STACK; + } else { + $p = undef; + last STACK; + } + } + } + + if ($p) { + my @prefetch = $self->result_source->resolve_prefetch( + $p, $attrs->{alias}, {}, \@pre_order, $collapse + ); + + push(@{$attrs->{select}}, map { $_->[0] } @prefetch); + push(@{$attrs->{as}}, map { $_->[1] } @prefetch); + } + } + push(@{$attrs->{order_by}}, @pre_order); + } + $attrs->{collapse} = $collapse; + $self->{_attrs} = $attrs; +} + +sub _merge_attr { + my ($self, $a, $b, $is_prefetch) = @_; + + return $b unless $a; + if (ref $b eq 'HASH' && ref $a eq 'HASH') { + foreach my $key (keys %{$b}) { + if (exists $a->{$key}) { + $a->{$key} = $self->_merge_attr($a->{$key}, $b->{$key}, $is_prefetch); + } else { + $a->{$key} = $b->{$key}; + } + } + return $a; + } else { + $a = [$a] unless (ref $a eq 'ARRAY'); + $b = [$b] unless (ref $b eq 'ARRAY'); + + my $hash = {}; + my $array = []; + foreach ($a, $b) { + foreach my $element (@{$_}) { + if (ref $element eq 'HASH') { + $hash = $self->_merge_attr($hash, $element, $is_prefetch); + } elsif (ref $element eq 'ARRAY') { + $array = [@{$array}, @{$element}]; + } else { + if (($b == $_) && $is_prefetch) { + $self->_merge_array($array, $element, $is_prefetch); + } else { + push(@{$array}, $element); + } + } + } + } + + my $final_array = []; + foreach my $element (@{$array}) { + push(@{$final_array}, $element) unless (exists $hash->{$element}); + } + $array = $final_array; + + if ((keys %{$hash}) && (scalar(@{$array} > 0))) { + return [$hash, @{$array}]; + } else { + return (keys %{$hash}) ? $hash : $array; + } + } +} + +sub _merge_array { + my ($self, $a, $b) = @_; + + $b = [$b] unless (ref $b eq 'ARRAY'); + # add elements from @{$b} to @{$a} which aren't already in @{$a} + foreach my $b_element (@{$b}) { + push(@{$a}, $b_element) unless grep {$b_element eq $_} @{$a}; + } +} + +sub _construct_object { + my ($self, @row) = @_; + my @as = @{ $self->{_attrs}{as} }; + + my $info = $self->_collapse_result(\@as, \@row); + my $new = $self->result_class->inflate_result($self->result_source, @$info); + $new = $self->{_attrs}{record_filter}->($new) + if exists $self->{_attrs}{record_filter}; + return $new; +} + +sub _collapse_result { + my ($self, $as, $row, $prefix) = @_; + + my $live_join = $self->{attrs}->{_live_join} ||=""; + my %const; + + my @copy = @$row; + foreach my $this_as (@$as) { + my $val = shift @copy; + if (defined $prefix) { + if ($this_as =~ m/^\Q${prefix}.\E(.+)$/) { + my $remain = $1; + $remain =~ /^(?:(.*)\.)?([^.]+)$/; + $const{$1||''}{$2} = $val; + } + } else { + $this_as =~ /^(?:(.*)\.)?([^.]+)$/; + $const{$1||''}{$2} = $val; + } + } + + my $info = [ {}, {} ]; + foreach my $key (keys %const) { + if (length $key && $key ne $live_join) { + my $target = $info; + my @parts = split(/\./, $key); + foreach my $p (@parts) { + $target = $target->[1]->{$p} ||= []; + } + $target->[0] = $const{$key}; + } else { + $info->[0] = $const{$key}; + } + } + my @collapse; + + if (defined $prefix) { + @collapse = map { + m/^\Q${prefix}.\E(.+)$/ ? ($1) : () + } keys %{$self->{_attrs}->{collapse}} + } else { + @collapse = keys %{$self->{_attrs}->{collapse}}; + }; + + if (@collapse) { + my ($c) = sort { length $a <=> length $b } @collapse; + my $target = $info; + foreach my $p (split(/\./, $c)) { + $target = $target->[1]->{$p} ||= []; + } + my $c_prefix = (defined($prefix) ? "${prefix}.${c}" : $c); + my @co_key = @{$self->{_attrs}->{collapse}{$c_prefix}}; + my $tree = $self->_collapse_result($as, $row, $c_prefix); + my %co_check = map { ($_, $tree->[0]->{$_}); } @co_key; + my (@final, @raw); + + while ( + !( + grep { + !defined($tree->[0]->{$_}) || $co_check{$_} ne $tree->[0]->{$_} + } @co_key + ) + ) { + push(@final, $tree); + last unless (@raw = $self->cursor->next); + $row = $self->{stashed_row} = \@raw; + $tree = $self->_collapse_result($as, $row, $c_prefix); + } + @$target = (@final ? @final : [ {}, {} ]); + # single empty result to indicate an empty prefetched has_many + } + + #print "final info: " . Dumper($info); + return $info; +} + +=head2 result_source + +=over 4 + +=item Arguments: $result_source? + +=item Return Value: $result_source + +=back + +An accessor for the primary ResultSource object from which this ResultSet +is derived. + +=cut + + +=head2 count + +=over 4 + +=item Arguments: $cond, \%attrs?? + +=item Return Value: $count + +=back + +Performs an SQL C with the same query as the resultset was built +with to find the number of elements. If passed arguments, does a search +on the resultset and counts the results of that. + +Note: When using C with C, L emulates C +using C. Some databases (notably SQLite) do +not support C with multiple columns. If you are using such a +database, you should only use columns from the main table in your C +clause. + +=cut + +sub count { + my $self = shift; + return $self->search(@_)->count if @_ and defined $_[0]; + return scalar @{ $self->get_cache } if $self->get_cache; + my $count = $self->_count; + return 0 unless $count; + + $count -= $self->{attrs}{offset} if $self->{attrs}{offset}; + $count = $self->{attrs}{rows} if + $self->{attrs}{rows} and $self->{attrs}{rows} < $count; + return $count; +} + +sub _count { # Separated out so pager can get the full count + my $self = shift; + my $select = { count => '*' }; + + $self->_resolve; + my $attrs = { %{ $self->{_attrs} } }; + if (my $group_by = delete $attrs->{group_by}) { + delete $attrs->{having}; + my @distinct = (ref $group_by ? @$group_by : ($group_by)); + # todo: try CONCAT for multi-column pk + my @pk = $self->result_source->primary_columns; + if (@pk == 1) { + foreach my $column (@distinct) { + if ($column =~ qr/^(?:\Q$attrs->{alias}.\E)?$pk[0]$/) { + @distinct = ($column); + last; + } + } + } + + $select = { count => { distinct => \@distinct } }; + } + + $attrs->{select} = $select; + $attrs->{as} = [qw/count/]; + + # offset, order by and page are not needed to count. record_filter is cdbi + delete $attrs->{$_} for qw/rows offset order_by page pager record_filter/; + my $tmp_rs = (ref $self)->new($self->result_source, $attrs); + $tmp_rs->{_parent_rs} = $self->{_parent_rs} if ($self->{_parent_rs}); + #XXX - hack to pass through parent of related resultsets + + my ($count) = $tmp_rs->cursor->next; + return $count; +} + +=head2 count_literal + +=over 4 + +=item Arguments: $sql_fragment, @bind_values + +=item Return Value: $count + +=back + +Counts the results in a literal query. Equivalent to calling L +with the passed arguments, then L. + +=cut + +sub count_literal { shift->search_literal(@_)->count; } + +=head2 all + +=over 4 + +=item Arguments: none + +=item Return Value: @objects + +=back + +Returns all elements in the resultset. Called implicitly if the resultset +is returned in list context. + +=cut + +sub all { + my ($self) = @_; + return @{ $self->get_cache } if $self->get_cache; + + my @obj; + + # TODO: don't call resolve here + $self->_resolve; + if (keys %{$self->{_attrs}->{collapse}}) { +# if ($self->{attrs}->{prefetch}) { + # Using $self->cursor->all is really just an optimisation. + # If we're collapsing has_many prefetches it probably makes + # very little difference, and this is cleaner than hacking + # _construct_object to survive the approach + my @row = $self->cursor->next; + while (@row) { + push(@obj, $self->_construct_object(@row)); + @row = (exists $self->{stashed_row} + ? @{delete $self->{stashed_row}} + : $self->cursor->next); + } + } else { + @obj = map { $self->_construct_object(@$_) } $self->cursor->all; + } + + $self->set_cache(\@obj) if $self->{attrs}{cache}; + return @obj; +} + +=head2 reset + +=over 4 + +=item Arguments: none + +=item Return Value: $self + +=back + +Resets the resultset's cursor, so you can iterate through the elements again. + +=cut + +sub reset { + my ($self) = @_; + delete $self->{_attrs} if (exists $self->{_attrs}); + + $self->{all_cache_position} = 0; + $self->cursor->reset; + return $self; +} + +=head2 first + +=over 4 + +=item Arguments: none + +=item Return Value: $object? + +=back + +Resets the resultset and returns an object for the first result (if the +resultset returns anything). + +=cut + +sub first { + return $_[0]->reset->next; +} + +# _cond_for_update_delete +# +# update/delete require the condition to be modified to handle +# the differing SQL syntax available. This transforms the $self->{cond} +# appropriately, returning the new condition. + +sub _cond_for_update_delete { + my ($self) = @_; + my $cond = {}; + + if (!ref($self->{cond})) { + # No-op. No condition, we're updating/deleting everything + } + elsif (ref $self->{cond} eq 'ARRAY') { + $cond = [ + map { + my %hash; + foreach my $key (keys %{$_}) { + $key =~ /([^.]+)$/; + $hash{$1} = $_->{$key}; + } + \%hash; + } @{$self->{cond}} + ]; + } + elsif (ref $self->{cond} eq 'HASH') { + if ((keys %{$self->{cond}})[0] eq '-and') { + $cond->{-and} = []; + + my @cond = @{$self->{cond}{-and}}; + for (my $i = 0; $i <= @cond - 1; $i++) { + my $entry = $cond[$i]; + + my %hash; + if (ref $entry eq 'HASH') { + foreach my $key (keys %{$entry}) { + $key =~ /([^.]+)$/; + $hash{$1} = $entry->{$key}; + } + } + else { + $entry =~ /([^.]+)$/; + $hash{$1} = $cond[++$i]; + } + + push @{$cond->{-and}}, \%hash; + } + } + else { + foreach my $key (keys %{$self->{cond}}) { + $key =~ /([^.]+)$/; + $cond->{$1} = $self->{cond}{$key}; + } + } + } + else { + $self->throw_exception( + "Can't update/delete on resultset with condition unless hash or array" + ); + } + + return $cond; +} + + +=head2 update + +=over 4 + +=item Arguments: \%values + +=item Return Value: $storage_rv + +=back + +Sets the specified columns in the resultset to the supplied values in a +single query. Return value will be true if the update succeeded or false +if no records were updated; exact type of success value is storage-dependent. + +=cut + +sub update { + my ($self, $values) = @_; + $self->throw_exception("Values for update must be a hash") + unless ref $values eq 'HASH'; + + my $cond = $self->_cond_for_update_delete; + + return $self->result_source->storage->update( + $self->result_source->from, $values, $cond + ); +} + +=head2 update_all + +=over 4 + +=item Arguments: \%values + +=item Return Value: 1 + +=back + +Fetches all objects and updates them one at a time. Note that C +will run DBIC cascade triggers, while L will not. + +=cut + +sub update_all { + my ($self, $values) = @_; + $self->throw_exception("Values for update must be a hash") + unless ref $values eq 'HASH'; + foreach my $obj ($self->all) { + $obj->set_columns($values)->update; + } + return 1; +} + +=head2 delete + +=over 4 + +=item Arguments: none + +=item Return Value: 1 + +=back + +Deletes the contents of the resultset from its result source. Note that this +will not run DBIC cascade triggers. See L if you need triggers +to run. + +=cut + +sub delete { + my ($self) = @_; + my $del = {}; + + my $cond = $self->_cond_for_update_delete; + + $self->result_source->storage->delete($self->result_source->from, $cond); + return 1; +} + +=head2 delete_all + +=over 4 + +=item Arguments: none + +=item Return Value: 1 + +=back + +Fetches all objects and deletes them one at a time. Note that C +will run DBIC cascade triggers, while L will not. + +=cut + +sub delete_all { + my ($self) = @_; + $_->delete for $self->all; + return 1; +} + +=head2 pager + +=over 4 + +=item Arguments: none + +=item Return Value: $pager + +=back + +Return Value a L object for the current resultset. Only makes +sense for queries with a C attribute. + +=cut + +sub pager { + my ($self) = @_; + my $attrs = $self->{attrs}; + $self->throw_exception("Can't create pager for non-paged rs") + unless $self->{page}; + $attrs->{rows} ||= 10; + return $self->{pager} ||= Data::Page->new( + $self->_count, $attrs->{rows}, $self->{page}); +} + +=head2 page + +=over 4 + +=item Arguments: $page_number + +=item Return Value: $rs + +=back + +Returns a resultset for the $page_number page of the resultset on which page +is called, where each page contains a number of rows equal to the 'rows' +attribute set on the resultset (10 by default). + +=cut + +sub page { + my ($self, $page) = @_; + my $attrs = { %{$self->{attrs}} }; + $attrs->{page} = $page; + return (ref $self)->new($self->result_source, $attrs); +} + +=head2 new_result + +=over 4 + +=item Arguments: \%vals + +=item Return Value: $object + +=back + +Creates an object in the resultset's result class and returns it. + +=cut + +sub new_result { + my ($self, $values) = @_; + $self->throw_exception( "new_result needs a hash" ) + unless (ref $values eq 'HASH'); + $self->throw_exception( + "Can't abstract implicit construct, condition not a hash" + ) if ($self->{cond} && !(ref $self->{cond} eq 'HASH')); + my %new = %$values; + my $alias = $self->{attrs}{alias}; + foreach my $key (keys %{$self->{cond}||{}}) { + $new{$1} = $self->{cond}{$key} if ($key =~ m/^(?:\Q${alias}.\E)?([^.]+)$/); + } + my $obj = $self->result_class->new(\%new); + $obj->result_source($self->result_source) if $obj->can('result_source'); + return $obj; +} + +=head2 find_or_new + +=over 4 + +=item Arguments: \%vals, \%attrs? + +=item Return Value: $object + +=back + +Find an existing record from this resultset. If none exists, instantiate a new +result object and return it. The object will not be saved into your storage +until you call L on it. + +If you want objects to be saved immediately, use L instead. + +=cut + +sub find_or_new { + my $self = shift; + my $attrs = (@_ > 1 && ref $_[$#_] eq 'HASH' ? pop(@_) : {}); + my $hash = ref $_[0] eq 'HASH' ? shift : {@_}; + my $exists = $self->find($hash, $attrs); + return defined $exists ? $exists : $self->new_result($hash); +} + +=head2 create + +=over 4 + +=item Arguments: \%vals + +=item Return Value: $object + +=back + +Inserts a record into the resultset and returns the object representing it. + +Effectively a shortcut for C<< ->new_result(\%vals)->insert >>. + +=cut + +sub create { + my ($self, $attrs) = @_; + $self->throw_exception( "create needs a hashref" ) + unless ref $attrs eq 'HASH'; + return $self->new_result($attrs)->insert; +} + +=head2 find_or_create + +=over 4 + +=item Arguments: \%vals, \%attrs? + +=item Return Value: $object + +=back + + $class->find_or_create({ key => $val, ... }); + +Tries to find a record based on its primary key or unique constraint; if none +is found, creates one and returns that instead. + + my $cd = $schema->resultset('CD')->find_or_create({ + cdid => 5, + artist => 'Massive Attack', + title => 'Mezzanine', + year => 2005, + }); + +Also takes an optional C attribute, to search by a specific key or unique +constraint. For example: + + my $cd = $schema->resultset('CD')->find_or_create( + { + artist => 'Massive Attack', + title => 'Mezzanine', + }, + { key => 'cd_artist_title' } + ); + +See also L and L. For information on how to declare +unique constraints, see L. + +=cut + +sub find_or_create { + my $self = shift; + my $attrs = (@_ > 1 && ref $_[$#_] eq 'HASH' ? pop(@_) : {}); + my $hash = ref $_[0] eq 'HASH' ? shift : {@_}; + my $exists = $self->find($hash, $attrs); + return defined $exists ? $exists : $self->create($hash); +} + +=head2 update_or_create + +=over 4 + +=item Arguments: \%col_values, { key => $unique_constraint }? + +=item Return Value: $object + +=back + + $class->update_or_create({ col => $val, ... }); + +First, searches for an existing row matching one of the unique constraints +(including the primary key) on the source of this resultset. If a row is +found, updates it with the other given column values. Otherwise, creates a new +row. + +Takes an optional C attribute to search on a specific unique constraint. +For example: + + # In your application + my $cd = $schema->resultset('CD')->update_or_create( + { + artist => 'Massive Attack', + title => 'Mezzanine', + year => 1998, + }, + { key => 'cd_artist_title' } + ); + +If no C is specified, it searches on all unique constraints defined on the +source, including the primary key. + +If the C is specified as C, it searches only on the primary key. + +See also L and L. For information on how to declare +unique constraints, see L. + +=cut + +sub update_or_create { + my $self = shift; + my $attrs = (@_ > 1 && ref $_[$#_] eq 'HASH' ? pop(@_) : {}); + my $cond = ref $_[0] eq 'HASH' ? shift : {@_}; + + my $row = $self->find($cond); + if (defined $row) { + $row->update($cond); + return $row; + } + + return $self->create($cond); +} + +=head2 get_cache + +=over 4 + +=item Arguments: none + +=item Return Value: \@cache_objects? + +=back + +Gets the contents of the cache for the resultset, if the cache is set. + +=cut + +sub get_cache { + shift->{all_cache}; +} + +=head2 set_cache + +=over 4 + +=item Arguments: \@cache_objects + +=item Return Value: \@cache_objects + +=back + +Sets the contents of the cache for the resultset. Expects an arrayref +of objects of the same class as those produced by the resultset. Note that +if the cache is set the resultset will return the cached objects rather +than re-querying the database even if the cache attr is not set. + +=cut + +sub set_cache { + my ( $self, $data ) = @_; + $self->throw_exception("set_cache requires an arrayref") + if defined($data) && (ref $data ne 'ARRAY'); + $self->{all_cache} = $data; +} + +=head2 clear_cache + +=over 4 + +=item Arguments: none + +=item Return Value: [] + +=back + +Clears the cache for the resultset. + +=cut + +sub clear_cache { + shift->set_cache(undef); +} + +=head2 related_resultset + +=over 4 + +=item Arguments: $relationship_name + +=item Return Value: $resultset + +=back + +Returns a related resultset for the supplied relationship name. + + $artist_rs = $schema->resultset('CD')->related_resultset('Artist'); + +=cut + +sub related_resultset { + my ( $self, $rel ) = @_; + + $self->{related_resultsets} ||= {}; + return $self->{related_resultsets}{$rel} ||= do { + #warn "fetching related resultset for rel '$rel' " . $self->result_source->{name}; + my $rel_obj = $self->result_source->relationship_info($rel); + #print Dumper($self->result_source->_relationships); + $self->throw_exception( + "search_related: result source '" . $self->result_source->name . + "' has no such relationship ${rel}") + unless $rel_obj; #die Dumper $self->{attrs}; + + my @live_join_stack = ( + exists $self->{attrs}->{_live_join_stack}) + ? @{$self->{attrs}->{_live_join_stack}} + : (); + + push(@live_join_stack, $rel); + + my $rs = $self->result_source->schema->resultset($rel_obj->{class})->search( + undef, { + select => undef, + as => undef, + _live_join => $rel, #the most recent + _live_join_stack => \@live_join_stack, #the trail of rels + _parent_attrs => $self->{attrs}} + ); + + # keep reference of the original resultset + $rs->{_parent_rs} = ($self->{_parent_rs}) + ? $self->{_parent_rs} + : $self->result_source; + + return $rs; + }; +} + +=head2 throw_exception + +See L for details. + +=cut + +sub throw_exception { + my $self=shift; + $self->result_source->schema->throw_exception(@_); +} + +# XXX: FIXME: Attributes docs need clearing up + +=head1 ATTRIBUTES + +The resultset takes various attributes that modify its behavior. Here's an +overview of them: + +=head2 order_by + +=over 4 + +=item Value: ($order_by | \@order_by) + +=back + +Which column(s) to order the results by. This is currently passed +through directly to SQL, so you can give e.g. C for a +descending order on the column `year'. + +Please note that if you have quoting enabled (see +L) you will need to do C<\'year DESC' > to +specify an order. (The scalar ref causes it to be passed as raw sql to the DB, +so you will need to manually quote things as appropriate.) + +=head2 columns + +=over 4 + +=item Value: \@columns + +=back + +Shortcut to request a particular set of columns to be retrieved. Adds +C onto the start of any column without a C<.> in it and sets C as normal. (You may also +use the C attribute, as in earlier versions of DBIC.) + +=head2 include_columns + +=over 4 + +=item Value: \@columns + +=back + +Shortcut to include additional columns in the returned results - for example + + $schema->resultset('CD')->search(undef, { + include_columns => ['artist.name'], + join => ['artist'] + }); + +would return all CDs and include a 'name' column to the information +passed to object inflation + +=head2 select + +=over 4 + +=item Value: \@select_columns + +=back + +Indicates which columns should be selected from the storage. You can use +column names, or in the case of RDBMS back ends, function or stored procedure +names: + + $rs = $schema->resultset('Employee')->search(undef, { + select => [ + 'name', + { count => 'employeeid' }, + { sum => 'salary' } + ] + }); + +When you use function/stored procedure names and do not supply an C +attribute, the column names returned are storage-dependent. E.g. MySQL would +return a column named C in the above example. + +=head2 +select + +=over 4 + +Indicates additional columns to be selected from storage. Works the same as +L, usually when C