Fix $object->search_related aliasing, change semantics of _resolve_condition
[dbsrgits/DBIx-Class.git] / lib / DBIx / Class / Relationship / Base.pm
index 1a46d4e..7100ea1 100644 (file)
@@ -417,14 +417,8 @@ sub related_resultset {
 
     # condition resolution may fail if an incomplete master-object prefetch
     # is encountered - that is ok during prefetch construction (not yet in_storage)
-
-    # if $rel_info->{cond} is a CODE, we might need to join from the
-    # current resultsource instead of just querying the target
-    # resultsource, in that case, the condition might provide an
-    # additional condition in order to avoid an unecessary join if
-    # that is at all possible.
-    my ($cond, $extended_cond) = try {
-      $source->_resolve_condition( $rel_info->{cond}, $rel, $self )
+    my ($cond, $is_crosstable) = try {
+      $source->_resolve_condition( $rel_info->{cond}, $rel, $self, $rel )
     }
     catch {
       if ($self->in_storage) {
@@ -434,70 +428,72 @@ sub related_resultset {
       $DBIx::Class::ResultSource::UNRESOLVABLE_CONDITION;  # RV
     };
 
-    if ($cond eq $DBIx::Class::ResultSource::UNRESOLVABLE_CONDITION) {
-      my $reverse = $source->reverse_relationship_info($rel);
-      foreach my $rev_rel (keys %$reverse) {
-        if ($reverse->{$rev_rel}{attrs}{accessor} && $reverse->{$rev_rel}{attrs}{accessor} eq 'multi') {
-          $attrs->{related_objects}{$rev_rel} = [ $self ];
-          weaken $attrs->{related_object}{$rev_rel}[0];
-        } else {
-          $attrs->{related_objects}{$rev_rel} = $self;
-          weaken $attrs->{related_object}{$rev_rel};
-        }
-      }
+    # keep in mind that the following if() block is part of a do{} - no return()s!!!
+    if ($is_crosstable) {
+      $self->throw_exception (
+        "A cross-table relationship condition returned for statically declared '$rel'")
+          unless ref $rel_info->{cond} eq 'CODE';
+
+      # A WHOREIFFIC hack to reinvoke the entire condition resolution
+      # with the correct alias. Another way of doing this involves a
+      # lot of state passing around, and the @_ positions are already
+      # mapped out, making this crap a less icky option.
+      #
+      # The point of this exercise is to retain the spirit of the original
+      # $obj->search_related($rel) where the resulting rset will have the
+      # root alias as 'me', instead of $rel (as opposed to invoking
+      # $rs->search_related)
+
+
+      local $source->{_relationships}{me} = $source->{_relationships}{$rel};  # make the fake 'me' rel
+      my $obj_table_alias = lc($source->source_name) . '__row';
+
+      $source->resultset->search(
+        $self->ident_condition($obj_table_alias),
+        { alias => $obj_table_alias },
+      )->search_related('me', $query, $attrs)
     }
-
-    # this is where we're going to check if we have an extended
-    # rel. In that case, we need to: 1) If there's a second
-    # condition, we use that instead.  2) If there is only one
-    # condition, we need to join the current resultsource and have
-    # additional conditions.
-    if (ref $rel_info->{cond} eq 'CODE') {
-      # this is an extended relationship.
-      if ($extended_cond) {
-        $cond = $extended_cond;
-
-      } else {
-
-        # it's a bit hard to find out what to do with other joins
-        $self->throw_exception('Extended relationship '.$rel.' with additional join requires optimized declaration')
-          if exists $attrs->{join} && $attrs->{join};
-
-        # aliases get a bit more complicated, so we won't accept additional queries
-        $self->throw_exception('Extended relationship '.$rel.' with additional query requires optimized declaration')
-          if $query;
-
-        $attrs->{from} =
-          [ { $rel => $self->result_source->from },
-            [ { 'me' => $self->result_source->related_source($rel)->from }, { 1 => 1 } ] ];
-
-        $cond->{"${rel}.${_}"} = $self->get_column($_) for $self->result_source->primary_columns;
+    else {
+      # FIXME - this conditional doesn't seem correct - got to figure out
+      # at some point what it does. Also the entire UNRESOLVABLE_CONDITION
+      # business seems shady - we could simply not query *at all*
+      if ($cond eq $DBIx::Class::ResultSource::UNRESOLVABLE_CONDITION) {
+        my $reverse = $source->reverse_relationship_info($rel);
+        foreach my $rev_rel (keys %$reverse) {
+          if ($reverse->{$rev_rel}{attrs}{accessor} && $reverse->{$rev_rel}{attrs}{accessor} eq 'multi') {
+            $attrs->{related_objects}{$rev_rel} = [ $self ];
+            weaken $attrs->{related_object}{$rev_rel}[0];
+          } else {
+            $attrs->{related_objects}{$rev_rel} = $self;
+            weaken $attrs->{related_object}{$rev_rel};
+          }
+        }
       }
-    }
-
-    if (ref $cond eq 'ARRAY') {
-      $cond = [ map {
-        if (ref $_ eq 'HASH') {
-          my $hash;
-          foreach my $key (keys %$_) {
-            my $newkey = $key !~ /\./ ? "me.$key" : $key;
-            $hash->{$newkey} = $_->{$key};
+      elsif (ref $cond eq 'ARRAY') {
+        $cond = [ map {
+          if (ref $_ eq 'HASH') {
+            my $hash;
+            foreach my $key (keys %$_) {
+              my $newkey = $key !~ /\./ ? "me.$key" : $key;
+              $hash->{$newkey} = $_->{$key};
+            }
+            $hash;
+          } else {
+            $_;
           }
-          $hash;
-        } else {
-          $_;
+        } @$cond ];
+      }
+      elsif (ref $cond eq 'HASH') {
+       foreach my $key (grep { ! /\./ } keys %$cond) {
+          $cond->{"me.$key"} = delete $cond->{$key};
         }
-      } @$cond ];
-    } elsif (ref $cond eq 'HASH') {
-      foreach my $key (grep { ! /\./ } keys %$cond) {
-        $cond->{"me.$key"} = delete $cond->{$key};
       }
-    }
 
-    $query = ($query ? { '-and' => [ $cond, $query ] } : $cond);
-    $self->result_source->related_source($rel)->resultset->search(
-      $query, $attrs
-    );
+      $query = ($query ? { '-and' => [ $cond, $query ] } : $cond);
+      $self->result_source->related_source($rel)->resultset->search(
+        $query, $attrs
+      );
+    }
   };
 }
 
@@ -576,6 +572,40 @@ in L<DBIx::Class::ResultSet> for details.
 sub create_related {
   my $self = shift;
   my $rel = shift;
+
+  $self->throw_exception("Can't call *_related as class methods")
+    unless ref $self;
+
+  # we need to stop and check if this is at all possible. If this is
+  # an extended relationship with an incomplete definition, we should
+  # just forbid it right now.
+  my $rel_info = $self->result_source->relationship_info($rel);
+  if (ref $rel_info->{cond} eq 'CODE') {
+    my ($cond, $ext) = $rel_info->{cond}->({
+      self_alias => 'me',
+      foreign_alias => $rel,
+      self_rowobj => $self,
+      self_resultsource => $self->result_source,
+      foreign_relname => $rel,
+    });
+    $self->throw_exception("unable to set_from_related - no simplified condition available for '${rel}'")
+      unless $ext;
+
+    # now we need to make sure all non-identity relationship
+    # definitions are overriden.
+    my ($argref) = @_;
+    while ( my($col, $value) = each %$ext ) {
+      $col =~ s/^$rel\.//;
+      my $vref = ref $value;
+      if ($vref eq 'HASH') {
+        if (keys(%$value) && (keys %$value)[0] ne '=' &&
+            !exists $argref->{$col}) {
+          $self->throw_exception("unable to set_from_related via complex '${rel}' condition on column(s): '${col}'")
+        }
+      }
+    }
+  }
+
   my $obj = $self->search_related($rel)->create(@_);
   delete $self->{related_resultsets}->{$rel};
   return $obj;
@@ -662,28 +692,24 @@ set them in the storage.
 
 sub set_from_related {
   my ($self, $rel, $f_obj) = @_;
-  my $rel_info = $self->relationship_info($rel);
-  $self->throw_exception( "No such relationship ${rel}" ) unless $rel_info;
-  my $cond = $rel_info->{cond};
-  $self->throw_exception(
-    "set_from_related can only handle a hash condition; the ".
-    "condition for $rel is of type ".
-    (ref $cond ? ref $cond : 'plain scalar')
-  ) unless ref $cond eq 'HASH';
+
+  my $rel_info = $self->relationship_info($rel)
+    or $self->throw_exception( "No such relationship ${rel}" );
+
   if (defined $f_obj) {
     my $f_class = $rel_info->{class};
     $self->throw_exception( "Object $f_obj isn't a ".$f_class )
       unless blessed $f_obj and $f_obj->isa($f_class);
   }
 
-  # _resolve_condition might return two hashrefs, specially in the
-  # current case, since we know $f_object is an object.
-  my ($condref1, $condref2) = $self->result_source->_resolve_condition
-    ($rel_info->{cond}, $f_obj, $rel);
+  my ($cond, $crosstable) = $self->result_source->_resolve_condition
+    ($rel_info->{cond}, $f_obj, $rel, $rel);
+
+  $self->throw_exception(
+    "Custom relationship '$rel' does not resolve to a join-free condition fragment"
+  ) if $crosstable;
 
-  # if we get two condrefs, we need to use the second, otherwise we
-  # use the first.
-  $self->set_columns($condref2 ? $condref2 : $condref1);
+  $self->set_columns($cond);
 
   return 1;
 }