fixed some formatting issues
[dbsrgits/DBIx-Class.git] / lib / DBIx / Class / ResultSet.pm
index b2fa44e..759dec5 100644 (file)
@@ -263,7 +263,7 @@ a row by its primary key:
 You can also find a row by a specific unique constraint using the C<key>
 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:
 
@@ -272,14 +272,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<key> is specified and you explicitly name columns, it searches on all
-unique constraints defined on the source, including the primary key.
-
 If the C<key> is specified as C<primary>, it searches only on the primary key.
 
+If no C<key> is specified, it searches on all unique constraints defined on the
+source, including the primary key.
+
 See also L</find_or_create> and L</update_or_create>. For information on how to
 declare unique constraints, see
 L<DBIx::Class::ResultSource/add_unique_constraint>.
@@ -290,74 +290,77 @@ 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} = @_;
   }
-  elsif (@_) {
+  else {
     # Compatibility: Allow e.g. find(id => $value)
-    carp "find by key => value deprecated; please use a hashref instead";
-    $hash = {@_};
+    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->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")
-    );
+    $self->_resolve;  
+    return (keys %{$self->{_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) = @_;
 
-  # 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;
-  carp "find now requires a primary key or unique constraint; none is defined on "
-    . $self->result_source->name 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);
+
+    next unless scalar keys %$unique_query;
 
     # Add the ResultSet's alias
     foreach my $key (grep { ! m/\./ } keys %$unique_query) {
-      my $alias = $self->{attrs}->{alias};
+                       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 if %$unique_query;
-  }
-
-  # Compatibility: if we didn't get a unique query, take what the user provided
-  if (%$hash and not @unique_queries) {
-    carp "find now requires values for the primary key or a unique constraint"
-      . "; please use the search method instead";
-    push @unique_queries, $hash;
+    push @unique_queries, $unique_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;
-    return keys %{$rs->{_attrs}->{collapse}} ? $rs->next : $rs->single;
-  }
-  else {
-    $self->_resolve;  
-    return (keys %{$self->{_attrs}->{collapse}})
-      ? $self->search($query)->next
-      : $self->single($query);
-  }
+  return @unique_queries;
 }
 
 # _build_unique_query
@@ -460,12 +463,84 @@ sub single {
     }
   }
 
+  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);
+
+       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;
+}
+
 =head2 get_column
 
 =over 4
@@ -625,32 +700,32 @@ sub _resolve {
   $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;
-         }
+    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}));
+    }
+    
+    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 ];
- }
+  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}) {
@@ -681,58 +756,64 @@ sub _merge_attr {
     
   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} = delete $b->{$key};
-                       }
-               }
-               return $a;
+    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 $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;
-               }
+    $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 ($is_prefetch) {
+      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};
-       }
+  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)
@@ -852,7 +933,6 @@ 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;
 
@@ -890,7 +970,10 @@ 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_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;
 }
 
@@ -1302,8 +1385,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,
@@ -1320,7 +1403,7 @@ constraint. For example:
       artist => 'Massive Attack',
       title  => 'Mezzanine',
     },
-    { key => 'artist_title' }
+    { key => 'cd_artist_title' }
   );
 
 See also L</find> and L</update_or_create>. For information on how to declare
@@ -1363,7 +1446,7 @@ For example:
       title  => 'Mezzanine',
       year   => 1998,
     },
-    { key => 'artist_title' }
+    { key => 'cd_artist_title' }
   );
 
 If no C<key> is specified, it searches on all unique constraints defined on the
@@ -1379,15 +1462,15 @@ unique constraints, see L<DBIx::Class::ResultSource/add_unique_constraint>.
 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
@@ -1468,28 +1551,28 @@ Returns a related resultset for the supplied relationship name.
 
 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);
-      $self->throw_exception(
+    #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}
+    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;
+                      { %{$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;
   };
 }