X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FDBIx%2FClass%2FResultSet.pm;h=8e44f82ef4c29f8e42684333e1085cca799233c2;hb=8dc9c09f877e9719bef9470a5d376f0d5351786f;hp=c3a7bbeae8a071f89e09d413bf3b0a02351c93aa;hpb=447e7b286002ac0434d9ccb6601c3df56a1474e4;p=dbsrgits%2FDBIx-Class-Historic.git diff --git a/lib/DBIx/Class/ResultSet.pm b/lib/DBIx/Class/ResultSet.pm index c3a7bbe..8e44f82 100644 --- a/lib/DBIx/Class/ResultSet.pm +++ b/lib/DBIx/Class/ResultSet.pm @@ -6,8 +6,10 @@ use overload '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 DBIx::Class::ResultSetColumn; @@ -173,6 +175,11 @@ sub search_rs { } delete $attrs->{$key}; } + + if (exists $our_attrs->{prefetch}) { + $our_attrs->{join} = $self->_merge_attr($our_attrs->{join}, $our_attrs->{prefetch}, 1); + } + my $new_attrs = { %{$our_attrs}, %{$attrs} }; # merge new where and having into old @@ -268,11 +275,11 @@ Additionally, you can specify the columns explicitly by name: { key => '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. @@ -283,57 +290,41 @@ 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} = @_; - } - elsif (@_) { - # For backwards compatibility - $hash = {@_}; + $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 @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_queries = $self->_unique_queries($input_query, $attrs); +# use Data::Dumper; warn Dumper $self->result_source->name, $input_query, \@unique_queries, $self->{attrs}->{where}; - # 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}; - } - - push @unique_queries, $unique_query if %$unique_query; + # Verify the query + my $query = \@unique_queries; + if (scalar @unique_queries == 0) { + # Handle cases where the ResultSet defines the query, or where the user is + # abusing find + $query = $input_query; } - # 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); $rs->_resolve; @@ -347,6 +338,35 @@ sub find { } } +# _unique_queries +# +# Build a list of queries which satisfy unique constraints. + +sub _unique_queries { + my ($self, $query, $attrs) = @_; + + my @constraint_names = exists $attrs->{key} + ? ($attrs->{key}) + : $self->result_source->unique_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($query, \@unique_cols); + + next unless scalar keys %$unique_query; + + # Add the ResultSet's alias + foreach my $key (grep { ! m/\./ } keys %$unique_query) { + $unique_query->{"$self->{attrs}->{alias}.$key"} = delete $unique_query->{$key}; + } + + push @unique_queries, $unique_query; + } + + return @unique_queries; +} + # _build_unique_query # # Constrain the specified query hash based on the specified column names. @@ -425,6 +445,10 @@ 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 { @@ -443,12 +467,87 @@ sub single { } } +# use Data::Dumper; warn Dumper $attrs->{where}; + unless ($self->_is_unique_query($attrs->{where})) { + carp "Query not guarnteed 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); +# use Data::Dumper; warn Dumper $collapsed; + + foreach my $name ($self->result_source->unique_constraint_names) { + my @unique_cols = map { "$self->{attrs}->{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 = "$self->{attrs}->{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) = @_; + + # Accumulate fields in the AST + $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}; + } +# warn Dumper $collapsed; + } + } + + return $collapsed; +} + =head2 get_column =over 4 @@ -573,7 +672,6 @@ sub next { return $self->_construct_object(@row); } -# XXX - this is essentially just the old new(). rewrite / tidy up? sub _resolve { my $self = shift; @@ -582,7 +680,7 @@ sub _resolve { 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 + # 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); @@ -623,6 +721,18 @@ sub _resolve { $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}) { @@ -649,40 +759,56 @@ sub _resolve { } sub _merge_attr { - my ($self, $a, $b) = @_; + my ($self, $a, $b, $is_prefetch) = @_; + return $b unless $a; if (ref $b eq 'HASH' && ref $a eq 'HASH') { - return $self->_merge_hash($a, $b); + foreach my $key (keys %{$b}) { + if (exists $a->{$key}) { + $a->{$key} = $self->_merge_attr($a->{$key}, $b->{$key}, $is_prefetch); + } else { + $a->{$key} = delete $b->{$key}; + } + } + return $a; } else { - $a = [$a] unless (ref $a eq 'ARRAY'); - $b = [$b] unless (ref $b eq 'ARRAY'); - my @new_array = (@{$a}, @{$b}); - foreach my $a_element (@new_array) { - my $i = 0; - foreach my $b_element (@new_array) { - if ((ref $a_element eq 'HASH') && (ref $b_element eq 'HASH') && ($a_element ne $b_element)) { - $a_element = $self->_merge_hash($a_element, $b_element); - $new_array[$i] = undef; - } - $i++; - } - } - @new_array = grep($_, @new_array); - return \@new_array; - } + $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); + } + } + } + } + + if ((keys %{$hash}) && (scalar(@{$array} > 0))) { + return [$hash, @{$array}]; + } else { + return (keys %{$hash}) ? $hash : $array; + } + } } -sub _merge_hash { - my ($self, $a, $b) = @_; - - foreach my $key (keys %{$b}) { - if (exists $a->{$key}) { - $a->{$key} = $self->_merge_attr($a->{$key}, $b->{$key}); - } else { - $a->{$key} = delete $b->{$key}; - } - } - return $a; +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 { @@ -824,7 +950,7 @@ sub _count { # Separated out so pager can get the full count $self->_resolve; my $attrs = { %{ $self->{_attrs} } }; - if ($attrs->{distinct} && (my $group_by = $attrs->{group_by} || $attrs->{select})) { + 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 @@ -839,7 +965,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; @@ -984,7 +1109,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 - 1; $i++) { my $entry = $cond[$i]; my %hash; @@ -996,7 +1121,7 @@ sub _cond_for_update_delete { } else { $entry =~ /([^.]+)$/; - $hash{$entry} = $cond[++$i]; + $hash{$1} = $cond[++$i]; } push @{$cond->{-and}}, \%hash; @@ -1259,8 +1384,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, @@ -1336,15 +1461,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->update($hash); + $row->update($cond); return $row; } - return $self->create($hash); + return $self->create($cond); } =head2 get_cache @@ -1480,6 +1605,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 @@ -1535,6 +1665,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