X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FDBIx%2FClass%2FResultSet.pm;h=689586f3a64527113f90fcc7373e05a8bc9a606c;hb=7f613f3abf507feffb920890d245d325520d55c0;hp=aa76a6d36683e9e1048890c8ada2304a9755e257;hpb=6a588797377dd1e3b80b54c6e6d9b577512587cb;p=dbsrgits%2FDBIx-Class.git diff --git a/lib/DBIx/Class/ResultSet.pm b/lib/DBIx/Class/ResultSet.pm index aa76a6d..689586f 100644 --- a/lib/DBIx/Class/ResultSet.pm +++ b/lib/DBIx/Class/ResultSet.pm @@ -6,10 +6,9 @@ use overload '0+' => \&count, 'bool' => sub { 1; }, fallback => 1; +use Carp::Clan qw/^DBIx::Class/; use Data::Page; use Storable; -use Scalar::Util qw/weaken/; - use DBIx::Class::ResultSetColumn; use base qw/DBIx::Class/; __PACKAGE__->load_components(qw/AccessorGroup/); @@ -83,9 +82,9 @@ will return a CD object, not a ResultSet. sub new { my $class = shift; return $class->new_result(@_) if ref $class; - + my ($source, $attrs) = @_; - weaken $source; + #weaken $source; if ($attrs->{page}) { $attrs->{rows} ||= 10; @@ -94,6 +93,7 @@ sub new { } $attrs->{alias} ||= 'me'; + $attrs->{_orig_alias} ||= $attrs->{alias}; bless { result_source => $source, @@ -102,7 +102,6 @@ sub new { # from => $attrs->{from}, # collapse => $collapse, count => undef, - page => delete $attrs->{page}, pager => undef, attrs => $attrs }, $class; @@ -136,46 +135,111 @@ call it as C. sub search { my $self = shift; - - my $attrs = { %{$self->{attrs}} }; - my $having = delete $attrs->{having}; - $attrs = { %$attrs, %{ pop(@_) } } if @_ > 1 and ref $_[$#_] eq 'HASH'; + my $rs = $self->search_rs( @_ ); + return (wantarray ? $rs->all : $rs); +} + +=head2 search_rs + +=over 4 + +=item Arguments: $cond, \%attrs? + +=item Return Value: $resultset + +=back + +This method does the same exact thing as search() except it will +always return a resultset, even in list context. + +=cut +sub search_rs { + my $self = shift; + + my $rows; + + unless (@_) { # no search, effectively just a clone + $rows = $self->get_cache; + } + + my $attrs = {}; + $attrs = pop(@_) if @_ > 1 and ref $_[$#_] eq 'HASH'; + my $our_attrs = exists $attrs->{_parent_attrs} + ? { %{delete $attrs->{_parent_attrs}} } + : { %{$self->{attrs}} }; + my $having = delete $our_attrs->{having}; + + # XXX should only maintain _live_join_stack and generate _live_join_h from that + if ($attrs->{_live_join_stack}) { + foreach my $join (reverse @{$attrs->{_live_join_stack}}) { + $attrs->{_live_join_h} = defined $attrs->{_live_join_h} + ? { $join => $attrs->{_live_join_h} } + : $join; + } + } + + # merge new attrs into inherited + foreach my $key (qw/join prefetch/) { + next unless exists $attrs->{$key}; + if (my $live_join = $attrs->{_live_join_stack} || $our_attrs->{_live_join_stack}) { + foreach my $join (reverse @{$live_join}) { + $attrs->{$key} = { $join => $attrs->{$key} }; + } + } + + $our_attrs->{$key} = $self->_merge_attr($our_attrs->{$key}, delete $attrs->{$key}); + } + + $our_attrs->{join} = $self->_merge_attr( + $our_attrs->{join}, $attrs->{_live_join_h} + ) if ($attrs->{_live_join_h}); + + 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()); + ? ( + (@_ == 1 || ref $_[0] eq "HASH") + ? shift + : ( + (@_ % 2) + ? $self->throw_exception("Odd number of arguments to search") + : {@_} + ) + ) + : undef + ); + if (defined $where) { - $attrs->{where} = (defined $attrs->{where} - ? { '-and' => - [ map { ref $_ eq 'ARRAY' ? [ -or => $_ ] : $_ } - $where, $attrs->{where} ] } - : $where); + $new_attrs->{where} = ( + defined $new_attrs->{where} + ? { '-and' => [ + map { + ref $_ eq 'ARRAY' ? [ -or => $_ ] : $_ + } $where, $new_attrs->{where} + ] + } + : $where); } if (defined $having) { - $attrs->{having} = (defined $attrs->{having} - ? { '-and' => - [ map { ref $_ eq 'ARRAY' ? [ -or => $_ ] : $_ } - $having, $attrs->{having} ] } - : $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, $attrs); - $rs->{_parent_rs} = $self->{_parent_rs} if ($self->{_parent_rs}); #XXX - hack to pass through parent of related resultsets + my $rs = (ref $self)->new($self->result_source, $new_attrs); + $rs->{_parent_source} = $self->{_parent_source} if $self->{_parent_source}; - unless (@_) { # no search, effectively just a clone - my $rows = $self->get_cache; - if( @{$rows} ) { - $rs->set_cache($rows); - } + if ($rows) { + $rs->set_cache($rows); } - - return (wantarray ? $rs->all : $rs); + return $rs; } =head2 search_literal @@ -196,7 +260,6 @@ resultset query. =cut -# TODO: needs fixing sub search_literal { my ($self, $cond, @vals) = @_; my $attrs = (ref $vals[$#vals] eq 'HASH' ? { %{ pop(@vals) } } : {}); @@ -222,7 +285,9 @@ a row by its primary key: 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 => 'artist_title' }); + my $cd = $schema->resultset('CD')->find('Massive Attack', 'Mezzanine', { + key => 'cd_artist_title' + }); Additionally, you can specify the columns explicitly by name: @@ -231,14 +296,14 @@ Additionally, you can specify the columns explicitly by name: artist => 'Massive Attack', title => 'Mezzanine', }, - { key => 'artist_title' } + { key => 'cd_artist_title' } ); -If no C is specified and you explicitly name columns, 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. +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. @@ -249,61 +314,82 @@ sub find { my $self = shift; my $attrs = (@_ > 1 && ref $_[$#_] eq 'HASH' ? pop(@_) : {}); - # Parse out a hash from input + # 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; - my $hash; + # Parse out a hashref from input + my $input_query; if (ref $_[0] eq 'HASH') { - $hash = { %{$_[0]} }; + $input_query = { %{$_[0]} }; } elsif (@_ == @cols) { - $hash = {}; - @{$hash}{@cols} = @_; + $input_query = {}; + @{$input_query}{@cols} = @_; } else { - $self->throw_exception( - "Arguments to find must be a hashref or match the number of columns in the " - . exists $attrs->{key} ? "$attrs->{key} unique constraint" : "primary key" - ); + # Compatibility: Allow e.g. find(id => $value) + carp "Find by key => value deprecated; please use a hashref instead"; + $input_query = {@_}; } - # Check the hash we just parsed against our source's unique constraints + 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); + return keys %{$rs->_resolved_attrs->{collapse}} ? $rs->next : $rs->single; + } + else { + return keys %{$self->_resolved_attrs->{collapse}} + ? $self->search($query)->next + : $self->single($query); + } +} + +# _unique_queries +# +# Build a list of queries which satisfy unique constraints. + +sub _unique_queries { + my ($self, $query, $attrs) = @_; + + my $alias = $self->{attrs}{alias}; my @constraint_names = exists $attrs->{key} ? ($attrs->{key}) : $self->result_source->unique_constraint_names; - $self->throw_exception( - "Can't find unless a primary key or unique constraint is defined" - ) unless @constraint_names; 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($hash, \@unique_cols); + my $unique_query = $self->_build_unique_query($query, \@unique_cols); + + my $num_query = scalar keys %$unique_query; + next unless $num_query; # Add the ResultSet's alias - foreach my $key (grep { ! m/\./ } keys %$unique_query) { - my $alias = $self->{attrs}->{alias}; - $unique_query->{"$alias.$key"} = delete $unique_query->{$key}; + foreach my $col (grep { ! m/\./ } keys %$unique_query) { + $unique_query->{"$alias.$col"} = delete $unique_query->{$col}; } - push @unique_queries, $unique_query if %$unique_query; + # XXX: Assuming quite a bit about $self->{attrs}{where} + my $num_cols = scalar @unique_cols; + my $num_where = exists $self->{attrs}{where} + ? scalar keys %{ $self->{attrs}{where} } + : 0; + push @unique_queries, $unique_query + if $num_query + $num_where == $num_cols; } - # Handle cases where the ResultSet already defines the query - my $query = @unique_queries ? \@unique_queries : undef; - - # Run the query - if (keys %$attrs) { - my $rs = $self->search($query, $attrs); - return $rs->{attrs}->{prefetch} ? $rs->next : $rs->single; - } - else { - return ($self->{attrs}->{prefetch}) - ? $self->search($query)->next - : $self->single($query); - } + return @unique_queries; } # _build_unique_query @@ -313,19 +399,18 @@ sub find { sub _build_unique_query { my ($self, $query, $unique_cols) = @_; - my %unique_query = + return { map { $_ => $query->{$_} } grep { exists $query->{$_} } - @$unique_cols; - - return \%unique_query; + @$unique_cols + }; } =head2 search_related =over 4 -=item Arguments: $cond, \%attrs? +=item Arguments: $rel, $cond, \%attrs? =item Return Value: $new_resultset @@ -362,8 +447,7 @@ L for more information. sub cursor { my ($self) = @_; - $self->_resolve; - my $attrs = { %{$self->{_attrs}} }; + my $attrs = { %{$self->_resolved_attrs} }; return $self->{cursor} ||= $self->result_source->storage->select($attrs->{from}, $attrs->{select}, $attrs->{where},$attrs); @@ -384,12 +468,15 @@ sub cursor { 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 single { my ($self, $where) = @_; - $self->_resolve; - my $attrs = { %{$self->{_attrs}} }; + my $attrs = { %{$self->_resolved_attrs} }; if ($where) { if (defined $attrs->{where}) { $attrs->{where} = { @@ -402,12 +489,86 @@ sub single { } } + 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); + $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}{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 =~ /\./ ? $key : "$alias.$key"; + next unless exists $seen{$aliased}; # Additional constraints are okay + $seen{$aliased} = scalar keys %{ $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 $col (keys %$query) { + my $value = $query->{$col}; + $collapsed->{$col}{$value}++; + } + } + } + + return $collapsed; +} + =head2 get_column =over 4 @@ -426,7 +587,6 @@ Returns a ResultSetColumn instance for $column based on $self sub get_column { my ($self, $column) = @_; - my $new = DBIx::Class::ResultSetColumn->new($self, $column); return $new; } @@ -508,7 +668,7 @@ Can be used to efficiently iterate over records in the resultset: print $cd->title; } -Note that you need to store the resultset object, and call C on it. +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. @@ -516,103 +676,185 @@ first record from the resultset. sub next { my ($self) = @_; - if (@{$self->{all_cache} || []}) { + if (my $cache = $self->get_cache) { $self->{all_cache_position} ||= 0; - return $self->{all_cache}->[$self->{all_cache_position}++]; + 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 + my @row = ( + exists $self->{stashed_row} + ? @{delete $self->{stashed_row}} + : $self->cursor->next ); return unless (@row); return $self->_construct_object(@row); } -# XXX - this is essentially just the old new(). rewrite / tidy up? -sub _resolve { +sub _resolved_attrs { my $self = shift; + return $self->{_attrs} if $self->{_attrs}; - 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 - this is a hack to prevent dclone dieing because of the code ref, get's put back in $attrs afterwards - my $record_filter = delete $attrs->{record_filter} if (defined $attrs->{record_filter}); - $attrs = Storable::dclone($attrs || {}); # { %{ $attrs || {} } }; - 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}; + my $attrs = { %{$self->{attrs}||{}} }; + my $source = $self->{_parent_source} || $self->{result_source}; + my $alias = $attrs->{_orig_alias}; + + # XXX - lose storable dclone + my $record_filter = delete $attrs->{record_filter}; + #$attrs = Storable::dclone($attrs || {}); # { %{ $attrs || {} } }; + + $attrs->{record_filter} = $record_filter if $record_filter; + + $attrs->{columns} ||= delete $attrs->{cols} if exists $attrs->{cols}; + if ($attrs->{columns}) { + delete $attrs->{as}; + } elsif (!$attrs->{select}) { + $attrs->{columns} = [ $self->{result_source}->columns ]; + } + + my $select_alias = $self->{attrs}{alias}; + $attrs->{select} ||= [ + map { m/\./ ? $_ : "${select_alias}.$_" } @{delete $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); + map { m/^\Q${alias}.\E(.+)$/ ? $1 : $_ } @{$attrs->{select}} + ]; + + my $adds; + if ($adds = delete $attrs->{include_columns}) { + $adds = [$adds] unless ref $adds eq 'ARRAY'; + push(@{$attrs->{select}}, @$adds); + push(@{$attrs->{as}}, map { m/([^.]+)$/; $1 } @$adds); + } + if ($adds = delete $attrs->{'+select'}) { + $adds = [$adds] unless ref $adds eq 'ARRAY'; + push(@{$attrs->{select}}, map { /\./ || ref $_ ? $_ : "${alias}.$_" } @$adds); + } + if (my $adds = delete $attrs->{'+as'}) { + $adds = [$adds] unless ref $adds eq 'ARRAY'; + push(@{$attrs->{as}}, @$adds); } - #use Data::Dumper; warn Dumper(@{$attrs}{qw/select as/}); $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; - } - } + if (exists $attrs->{join} || exists $attrs->{prefetch}) { + + my $join = delete $attrs->{join} || {}; + + if (defined $attrs->{prefetch}) { + $join = $self->_merge_attr( + $join, $attrs->{prefetch} + ); + } - push(@{$attrs->{from}}, $source->resolve_join($join, $attrs->{alias}, $attrs->{seen_join})); + push(@{$attrs->{from}}, + $source->resolve_join($join, $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 ($attrs->{order_by}) { + $attrs->{order_by} = [ $attrs->{order_by} ] unless ref $attrs->{order_by}; + } else { + $attrs->{order_by} ||= []; + } + 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}; - } - my @prefetch = $source->resolve_prefetch( - $p, $attrs->{alias}, {}, \@pre_order, $collapse); - push(@{$attrs->{select}}, map { $_->[0] } @prefetch); - push(@{$attrs->{as}}, map { $_->[1] } @prefetch); + my @pre_order; + foreach my $p (ref $prefetch eq 'ARRAY' ? @$prefetch : ($prefetch)) { + # bring joins back to level of current class + $p = $self->_reduce_joins($p, $attrs) if $attrs->{_live_join_stack}; + if ($p) { + my @prefetch = $self->result_source->resolve_prefetch( + $p, $alias, {}, \@pre_order, $collapse + ); + push(@{$attrs->{select}}, map { $_->[0] } @prefetch); + push(@{$attrs->{as}}, map { $_->[1] } @prefetch); } - push(@{$attrs->{order_by}}, @pre_order); + } + push(@{$attrs->{order_by}}, @pre_order); } $attrs->{collapse} = $collapse; - $attrs->{record_filter} = $record_filter if ($record_filter); - $self->{_attrs} = $attrs; + + return $self->{_attrs} = $attrs; +} + +sub _merge_attr { + my ($self, $a, $b) = @_; + 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}); + } 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 my $x ($a, $b) { + foreach my $element (@{$x}) { + if (ref $element eq 'HASH') { + $hash = $self->_merge_attr($hash, $element); + } elsif (ref $element eq 'ARRAY') { + push(@array, @{$element}); + } else { + push(@array, $element) unless $b == $x + && grep { $_ eq $element } @array; + } + } + } + + @array = grep { !exists $hash->{$_} } @array; + + return keys %{$hash} + ? ( scalar(@array) + ? [$hash, @array] + : $hash + ) + : \@array; + } +} + +# bring the joins (which are from the original class) to the level +# of the current class so that we can resolve them properly +sub _reduce_joins { + my ($self, $p, $attrs) = @_; + + STACK: + foreach my $join (@{$attrs->{_live_join_stack}}) { + if (ref $p eq 'HASH') { + return undef unless exists $p->{$join}; + $p = $p->{$join}; + } elsif (ref $p eq 'ARRAY') { + foreach my $pe (@{$p}) { + return undef if $pe eq $join; + if (ref $pe eq 'HASH' && exists $pe->{$join}) { + $p = $pe->{$join}; + next STACK; + } + } + return undef; + } else { + return undef; + } + } + return $p; } 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, $self->{_parent_rs}); + my $info = $self->_collapse_result($self->{_attrs}{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; @@ -621,10 +863,9 @@ sub _construct_object { 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) { @@ -639,9 +880,10 @@ sub _collapse_result { } } + my $alias = $self->{attrs}{alias}; my $info = [ {}, {} ]; foreach my $key (keys %const) { - if (length $key && $key ne $live_join) { + if (length $key && $key ne $alias) { my $target = $info; my @parts = split(/\./, $key); foreach my $p (@parts) { @@ -652,14 +894,14 @@ sub _collapse_result { $info->[0] = $const{$key}; } } - + my @collapse; if (defined $prefix) { @collapse = map { m/^\Q${prefix}.\E(.+)$/ ? ($1) : () - } keys %{$self->{_attrs}->{collapse}} + } keys %{$self->{_attrs}{collapse}} } else { - @collapse = keys %{$self->{_attrs}->{collapse}}; + @collapse = keys %{$self->{_attrs}{collapse}}; }; if (@collapse) { @@ -669,21 +911,28 @@ sub _collapse_result { $target = $target->[1]->{$p} ||= []; } my $c_prefix = (defined($prefix) ? "${prefix}.${c}" : $c); - my @co_key = @{$self->{_attrs}->{collapse}{$c_prefix}}; - my %co_check = map { ($_, $target->[0]->{$_}); } @co_key; + 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) ) { + + 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; + @$target = (@final ? @final : [ {}, {} ]); + # single empty result to indicate an empty prefetched has_many } + + #print "final info: " . Dumper($info); return $info; } @@ -728,8 +977,7 @@ clause. sub count { my $self = shift; return $self->search(@_)->count if @_ and defined $_[0]; - return scalar @{ $self->get_cache } if @{ $self->get_cache }; - + return scalar @{ $self->get_cache } if $self->get_cache; my $count = $self->_count; return 0 unless $count; @@ -742,17 +990,17 @@ sub count { sub _count { # Separated out so pager can get the full count my $self = shift; my $select = { count => '*' }; - - $self->_resolve; - my $attrs = { %{ $self->{_attrs} } }; + + my $attrs = { %{$self->_resolved_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) { + my $alias = $attrs->{_orig_alias}; foreach my $column (@distinct) { - if ($column =~ qr/^(?:\Q$attrs->{alias}.\E)?$pk[0]$/) { + if ($column =~ qr/^(?:\Q${alias}.\E)?$pk[0]$/) { @distinct = ($column); last; } @@ -760,7 +1008,6 @@ sub _count { # Separated out so pager can get the full count } $select = { count => { distinct => \@distinct } }; - #use Data::Dumper; die Dumper $select; } $attrs->{select} = $select; @@ -768,8 +1015,11 @@ sub _count { # Separated out so pager can get the full 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 ($count) = (ref $self)->new($self->result_source, $attrs)->cursor->next; + my $tmp_rs = (ref $self)->new($self->result_source, $attrs); + $tmp_rs->{_parent_source} = $self->{_parent_source} if $self->{_parent_source}; + #XXX - hack to pass through parent of related resultsets + + my ($count) = $tmp_rs->cursor->next; return $count; } @@ -807,14 +1057,13 @@ is returned in list context. sub all { my ($self) = @_; - return @{ $self->get_cache } if @{ $self->get_cache }; + return @{ $self->get_cache } if $self->get_cache; my @obj; - # XXX used to be 'if (keys %{$self->{collapse}})' - # XXX replaced by this as it seemed to do roughly the same thing - # XXX could be bad as never really understood exactly what collapse did - if ($self->{attrs}->{prefetch}) { + # TODO: don't call resolve here + if (keys %{$self->_resolved_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 @@ -850,8 +1099,7 @@ Resets the resultset's cursor, so you can iterate through the elements again. sub reset { my ($self) = @_; - delete $self->{_attrs} if (exists $self->{_attrs}); - + delete $self->{_attrs} if exists $self->{_attrs}; $self->{all_cache_position} = 0; $self->cursor->reset; return $self; @@ -886,10 +1134,10 @@ 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') { + # No-op. No condition, we're updating/deleting everything + return $cond unless ref $self->{cond}; + + if (ref $self->{cond} eq 'ARRAY') { $cond = [ map { my %hash; @@ -906,7 +1154,7 @@ sub _cond_for_update_delete { $cond->{-and} = []; my @cond = @{$self->{cond}{-and}}; - for (my $i = 0; $i < @cond - 1; $i++) { + for (my $i = 0; $i < @cond; $i++) { my $entry = $cond[$i]; my %hash; @@ -918,7 +1166,7 @@ sub _cond_for_update_delete { } else { $entry =~ /([^.]+)$/; - $hash{$entry} = $cond[++$i]; + $hash{$1} = $cond[++$i]; } push @{$cond->{-and}}, \%hash; @@ -1012,7 +1260,6 @@ to run. sub delete { my ($self) = @_; - my $del = {}; my $cond = $self->_cond_for_update_delete; @@ -1060,10 +1307,10 @@ sub pager { my ($self) = @_; my $attrs = $self->{attrs}; $self->throw_exception("Can't create pager for non-paged rs") - unless $self->{page}; + unless $self->{attrs}{page}; $attrs->{rows} ||= 10; return $self->{pager} ||= Data::Page->new( - $self->_count, $attrs->{rows}, $self->{page}); + $self->_count, $attrs->{rows}, $self->{attrs}{page}); } =head2 page @@ -1084,9 +1331,7 @@ attribute set on the resultset (10 by default). sub page { my ($self, $page) = @_; - my $attrs = { %{$self->{attrs}} }; - $attrs->{page} = $page; - return (ref $self)->new($self->result_source, $attrs); + return (ref $self)->new($self->result_source, { %{$self->{attrs}}, page => $page }); } =head2 new_result @@ -1111,7 +1356,7 @@ sub new_result { "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}; + my $alias = $self->{attrs}{_orig_alias}; foreach my $key (keys %{$self->{cond}||{}}) { $new{$1} = $self->{cond}{$key} if ($key =~ m/^(?:\Q${alias}.\E)?([^.]+)$/); } @@ -1181,8 +1426,8 @@ sub create { $class->find_or_create({ key => $val, ... }); -Searches for a record matching the search condition; if it doesn't find one, -creates one and returns that instead. +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, @@ -1199,7 +1444,7 @@ constraint. For example: artist => 'Massive Attack', title => 'Mezzanine', }, - { key => 'artist_title' } + { key => 'cd_artist_title' } ); See also L and L. For information on how to declare @@ -1242,7 +1487,7 @@ For example: title => 'Mezzanine', year => 1998, }, - { key => 'artist_title' } + { key => 'cd_artist_title' } ); If no C is specified, it searches on all unique constraints defined on the @@ -1258,16 +1503,15 @@ unique constraints, see L. sub update_or_create { my $self = shift; my $attrs = (@_ > 1 && ref $_[$#_] eq 'HASH' ? pop(@_) : {}); - my $hash = ref $_[0] eq 'HASH' ? shift : {@_}; + my $cond = ref $_[0] eq 'HASH' ? shift : {@_}; - my $row = $self->find($hash, $attrs); + my $row = $self->find($cond); if (defined $row) { - $row->set_columns($hash); - $row->update; + $row->update($cond); return $row; } - return $self->create($hash); + return $self->create($cond); } =head2 get_cache @@ -1285,7 +1529,7 @@ Gets the contents of the cache for the resultset, if the cache is set. =cut sub get_cache { - shift->{all_cache} || []; + shift->{all_cache}; } =head2 set_cache @@ -1308,13 +1552,7 @@ than re-querying the database even if the cache attr is not set. sub set_cache { my ( $self, $data ) = @_; $self->throw_exception("set_cache requires an arrayref") - if ref $data ne 'ARRAY'; - my $result_class = $self->result_class; - foreach( @$data ) { - $self->throw_exception( - "cannot cache object of type '$_', expected '$result_class'" - ) if ref $_ ne $result_class; - } + if defined($data) && (ref $data ne 'ARRAY'); $self->{all_cache} = $data; } @@ -1333,7 +1571,7 @@ Clears the cache for the resultset. =cut sub clear_cache { - shift->set_cache([]); + shift->set_cache(undef); } =head2 related_resultset @@ -1353,29 +1591,41 @@ Returns a related resultset for the supplied relationship name. =cut sub related_resultset { - my ( $self, $rel ) = @_; + 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); - $self->throw_exception( - "search_related: result source '" . $self->result_source->name . - "' has no such relationship ${rel}") - unless $rel_obj; #die Dumper $self->{attrs}; - - my $rs = $self->result_source->schema->resultset($rel_obj->{class} - )->search( undef, - { %{$self->{attrs}}, - select => undef, - as => undef, - join => $rel, - _live_join => $rel } - ); - - # keep reference of the original resultset - $rs->{_parent_rs} = $self->result_source; - return $rs; + my $rel_obj = $self->result_source->relationship_info($rel); + + $self->throw_exception( + "search_related: result source '" . $self->result_source->name . + "' has no such relationship $rel") + unless $rel_obj; + + my @live_join_stack = @{$self->{attrs}{_live_join_stack}||[]}; + + # XXX mst: I'm sure this is wrong, somehow + # something with complex joins early on could die on search_rel + # followed by a prefetch. I think. need a test case. + + my $join_count = scalar(grep { $_ eq $rel } @live_join_stack); + my $alias = $join_count ? join('_', $rel, $join_count+1) : $rel; + + push(@live_join_stack, $rel); + + my $rs = $self->result_source->schema->resultset($rel_obj->{class})->search( + undef, { + select => undef, + as => undef, + alias => $alias, + _live_join_stack => \@live_join_stack, + _parent_attrs => $self->{attrs}} + ); + + # keep reference of the original resultset + $rs->{_parent_source} = $self->{_parent_source} || $self->result_source; + + return $rs; }; } @@ -1409,6 +1659,11 @@ 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 @@ -1464,6 +1719,23 @@ 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