Another heavy refactor of the rel resolver prototype (sequel to 03f6d1f7)
Peter Rabbitson [Sat, 12 Jul 2014 15:32:52 +0000 (17:32 +0200)]
Change the return value of _resolve_relationship_condition to a 3-value
hash, with a lot of the safety logic consolidated within that method. The
best way to gauge the significance of the changes is to look at the diff
of lib/DBIx/Class/Relationship/Base.pm

Also stop returning the "noncondition-columns" in the _resolve_condition
compa-shim. The information was only used by the code removed from
::Relationship::Base, and is rather new with no evidence of use within
CPAN/DarkPAN. It can be easily added back if necessary.

lib/DBIx/Class/Relationship/Base.pm
lib/DBIx/Class/ResultSource.pm
t/relationship/custom.t

index 2868250..e0007ff 100644 (file)
@@ -631,39 +631,15 @@ your storage until you call L<DBIx::Class::Row/insert> on it.
 =cut
 
 sub new_related {
-  my ($self, $rel, $values) = @_;
-
-  # FIXME - this is a bad position for this (also an identical copy in
-  # set_from_related), but I have no saner way to hook, and I absolutely
-  # want this to throw at least for coderefs, instead of the "insert a NULL
-  # when it gets hard" insanity --ribasushi
-  #
-  # sanity check - currently throw when a complex coderef rel is encountered
-  # FIXME - should THROW MOAR!
-
-    my $rsrc = $self->result_source;
-    my $rel_info = $rsrc->relationship_info($rel)
-      or $self->throw_exception( "No such relationship '$rel'" );
-    my (undef, $crosstable, $nonequality_foreign_columns) = $rsrc->_resolve_condition (
-      $rel_info->{cond}, $rel, $self, $rel
-    );
-
-    $self->throw_exception("Relationship '$rel' does not resolve to a join-free condition fragment")
-      if $crosstable;
-
-    if (
-      $nonequality_foreign_columns
-        and
-      my @unspecified_rel_condition_chunks = grep { ! exists $values->{$_} } @$nonequality_foreign_columns
-    ) {
-      $self->throw_exception(sprintf (
-        "Custom relationship '%s' not definitive - returns conditions instead of values for column(s): %s",
-        $rel,
-        map { "'$_'" } @unspecified_rel_condition_chunks
-      ));
-    }
-
-  return $self->search_related($rel)->new_result($values);
+  my ($self, $rel, $data) = @_;
+
+  return $self->search_related($rel)->new_result( $self->result_source->_resolve_relationship_condition (
+    infer_values_based_on => $data,
+    rel_name => $rel,
+    self_resultobj => $self,
+    foreign_alias => $rel,
+    self_alias => 'me',
+  )->{inferred_values} );
 }
 
 =head2 create_related
@@ -805,37 +781,13 @@ set them in the storage.
 sub set_from_related {
   my ($self, $rel, $f_obj) = @_;
 
-  my $rsrc = $self->result_source;
-  my $rel_info = $rsrc->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);
-  }
-
-
-  # FIXME - this is a bad position for this (also an identical copy in
-  # new_related), but I have no saner way to hook, and I absolutely
-  # want this to throw at least for coderefs, instead of the "insert a NULL
-  # when it gets hard" insanity --ribasushi
-  #
-  # sanity check - currently throw when a complex coderef rel is encountered
-  # FIXME - should THROW MOAR!
-  my ($cond, $crosstable, $nonequality_foreign_columns) = $rsrc->_resolve_condition (
-    $rel_info->{cond}, $f_obj, $rel, $rel
-  );
-  $self->throw_exception("Relationship '$rel' does not resolve to a join-free condition fragment")
-    if $crosstable;
-
-  $self->throw_exception(sprintf (
-    "Custom relationship '%s' not definitive - returns conditions instead of values for column(s): %s",
-    $rel,
-    map { "'$_'" } @$nonequality_foreign_columns
-  )) if $nonequality_foreign_columns;
-
-  $self->set_columns($cond);
+  $self->set_columns( $self->result_source->_resolve_relationship_condition (
+    infer_values_based_on => {},
+    rel_name => $rel,
+    foreign_resultobj => $f_obj,
+    foreign_alias => $rel,
+    self_alias => 'me',
+  )->{inferred_values} );
 
   return 1;
 }
index 886f47e..c679634 100644 (file)
@@ -1729,11 +1729,15 @@ sub _resolve_condition {
 #######################
 
   # now it's fucking easy isn't it?!
-  my @res = $self->_resolve_relationship_condition( $args );
+  my $rc = $self->_resolve_relationship_condition( $args );
 
-  # FIXME - this is also insane, but just be consistent for now
-  # _resolve_relationship_condition always returns qualified cols
-  # even in the case of objects, but nothing downstream expects this
+  my @res = (
+    ( $rc->{join_free_condition} || $rc->{condition} ),
+    ! $rc->{join_free_condition},
+  );
+
+  # _resolve_relationship_condition always returns qualified cols even in the
+  # case of join_free_condition, but nothing downstream expects this
   if (ref $res[0] eq 'HASH' and ($is_objlike[0] or $is_objlike[1]) ) {
     $res[0] = { map
       { ($_ =~ /\.(.+)/) => $res[0]{$_} }
@@ -1741,7 +1745,7 @@ sub _resolve_condition {
     };
   }
 
-  # more legacy
+  # and more legacy
   return wantarray ? @res : $res[0];
 }
 
@@ -1754,31 +1758,51 @@ our $UNRESOLVABLE_CONDITION = UNRESOLVABLE_CONDITION;
 # we are moving to a constant
 Internals::SvREADONLY($UNRESOLVABLE_CONDITION => 1);
 
-# Resolves the passed condition to a concrete query fragment and a flag
-# indicating whether this is a cross-table condition. Also an optional
-# list of non-trivial values (normally conditions) returned as a part
-# of a joinfree condition hash
+# Resolves the passed condition to a concrete query fragment and extra
+# metadata
+#
+## self-explanatory API, modeled on the custom cond coderef:
+# rel_name
+# foreign_alias
+# foreign_resultobj
+# self_alias
+# self_resultobj
+# require_join_free_condition
+# infer_values_based_on (optional, mandatory hashref argument)
+# condition (optional, derived from $self->rel_info(rel_name))
+#
+## returns a hash
+# condition
+# join_free_condition (maybe undef)
+# inferred_values (maybe undef, always complete or empty)
+#
 sub _resolve_relationship_condition {
   my $self = shift;
 
-  # self-explanatory API, modeled on the custom cond coderef:
-  # condition
-  # rel_name
-  # foreign_alias
-  # foreign_resultobj
-  # self_alias
-  # self_resultobj
   my $args = { ref $_[0] eq 'HASH' ? %{ $_[0] } : @_ };
 
   for ( qw( rel_name self_alias foreign_alias ) ) {
-    $self->throw_exception("Mandatory attribute '$_' is not a plain string")
+    $self->throw_exception("Mandatory argument '$_' is not a plain string")
       if !defined $args->{$_} or length ref $args->{$_};
   }
 
   $self->throw_exception('No practical way to resolve a relationship between two objects')
     if defined $args->{self_resultobj} and defined $args->{foreign_resultobj};
 
-  $args->{condition} ||= $self->relationship_info($args->{rel_name})->{cond};
+  my $rel_info = $self->relationship_info($args->{rel_name});
+  #  or $self->throw_exception( "No such relationship '$args->{rel_name}'" );
+
+  $self->throw_exception( "Object '$args->{foreign_resultobj}' must be of class '$rel_info->{class}'" )
+    if defined blessed $args->{foreign_resultobj} and ! $args->{foreign_resultobj}->isa($rel_info->{class});
+
+  $args->{condition} ||= $rel_info->{cond};
+
+  $self->throw_exception( "Argument to infer_values_based_on must be a hash" )
+    if exists $args->{infer_values_based_on} and ref $args->{infer_values_based_on} ne 'HASH';
+
+  $args->{require_join_free_condition} ||= !!$args->{infer_values_based_on};
+
+  my $ret;
 
   if (ref $args->{condition} eq 'CODE') {
 
@@ -1787,22 +1811,29 @@ sub _resolve_relationship_condition {
       self_resultsource => $self,
       self_alias => $args->{self_alias},
       foreign_alias => $args->{foreign_alias},
-      self_resultobj => $args->{self_resultobj},
-      foreign_resultobj => $args->{foreign_resultobj},
+      ( map
+        { (exists $args->{$_}) ? ( $_ => $args->{$_} ) : () }
+        qw( self_resultobj foreign_resultobj )
+      ),
     };
 
     # legacy - never remove these!!!
     $cref_args->{foreign_relname} = $cref_args->{rel_name};
-    $cref_args->{self_rowobj} = $cref_args->{self_resultobj};
 
-    my ($crosstable_cond, $joinfree_cond, @extra) = $args->{condition}->($cref_args);
+    $cref_args->{self_rowobj} = $cref_args->{self_resultobj}
+      if exists $cref_args->{self_resultobj};
+
+    ($ret->{condition}, $ret->{join_free_condition}, my @extra) = $args->{condition}->($cref_args);
 
     # FIXME sanity check
     carp_unique('A custom condition coderef can return at most 2 conditions: extra return values discarded')
       if @extra;
 
-    my @nonvalue_cols;
-    if ($joinfree_cond) {
+    if (my $jfc = $ret->{join_free_condition}) {
+
+      $self->throw_exception (
+        "The join-free condition returned for relationship '$args->{rel_name}' must be a hash reference"
+      ) unless ref $jfc eq 'HASH';
 
       my ($joinfree_alias, $joinfree_source);
       if (defined $args->{self_resultobj}) {
@@ -1819,34 +1850,16 @@ sub _resolve_relationship_condition {
         "A join-free condition returned for relationship '$args->{rel_name}' without a result object to chain from"
       ) unless $joinfree_alias;
 
-      my $fq_col_list = { map { ( "$joinfree_alias.$_" => 1 ) } $joinfree_source->columns };
-
-      # FIXME another sanity check
-      if (
-        ref $joinfree_cond ne 'HASH'
-          or
-        grep { ! $fq_col_list->{$_} } keys %$joinfree_cond
-      ) {
-        $self->throw_exception (
-          "The join-free condition returned for relationship '$args->{rel_name}' must be a hash "
-         .'reference with all keys being fully qualified column names of the corresponding source'
-        );
-      }
-
-      # see which parts of the joinfree cond are *NOT* foreign-source-column equalities
-      my $joinfree_cond_equality_columns =
-        $self->schema->storage->_extract_fixed_condition_columns($joinfree_cond, 'consider_nulls');
+      my $fq_col_list = { map
+        { ( "$joinfree_alias.$_" => 1 ) }
+        $joinfree_source->columns
+      };
 
-      @nonvalue_cols = map
-        { $_ =~ /^\Q$joinfree_alias.\E(.+)/ }
-        grep
-          { ! exists $joinfree_cond_equality_columns->{$_} }
-          keys %$joinfree_cond;
+      $fq_col_list->{$_} or $self->throw_exception (
+        "The join-free condition returned for relationship '$args->{rel_name}' may only "
+      . 'contain keys that are fully qualified column names of the corresponding source'
+      ) for keys %$jfc;
 
-      return ($joinfree_cond, 0, (@nonvalue_cols ? \@nonvalue_cols : undef));
-    }
-    else {
-      return ($crosstable_cond, 1);
     }
   }
   elsif (ref $args->{condition} eq 'HASH') {
@@ -1869,16 +1882,13 @@ sub _resolve_relationship_condition {
       push @l_cols, $lc;
     }
 
-    # plain values
-    if (! defined $args->{self_resultobj} and ! defined $args->{foreign_resultobj}) {
-      return ( { map
-        {( "$args->{foreign_alias}.$f_cols[$_]" => { -ident => "$args->{self_alias}.$l_cols[$_]" } )}
-        (0..$#f_cols)
-      }, 1 ); # is crosstable
-    }
-    else {
+    # construct the crosstable condition
+    $ret->{condition} = { map
+      {( "$args->{foreign_alias}.$f_cols[$_]" => { -ident => "$args->{self_alias}.$l_cols[$_]" } )}
+      (0..$#f_cols)
+    };
 
-      my $cond;
+    if (exists $args->{self_resultobj} or exists $args->{foreign_resultobj}) {
 
       my ($obj, $obj_alias, $plain_alias, $obj_cols, $plain_cols) = defined $args->{self_resultobj}
         ? ( @{$args}{qw( self_resultobj self_alias foreign_alias )}, \@l_cols, \@f_cols )
@@ -1889,7 +1899,7 @@ sub _resolve_relationship_condition {
 
         # FIXME - temp shim
         if (! blessed $obj) {
-          $cond->{"$plain_alias.$plain_cols->[$i]"} = $obj->{$obj_cols->[$i]};
+          $ret->{join_free_condition}{"$plain_alias.$plain_cols->[$i]"} = $obj->{$obj_cols->[$i]};
         }
         elsif (
           defined $args->{self_resultobj}
@@ -1907,48 +1917,96 @@ sub _resolve_relationship_condition {
             $obj_cols->[$i],
           ) if $obj->in_storage;
 
-          return UNRESOLVABLE_CONDITION;
+          # FIXME - temporarly force-override
+          delete $args->{require_join_free_condition};
+          $ret->{join_free_condition} = UNRESOLVABLE_CONDITION;
+          last;
         }
         else {
-          $cond->{"$plain_alias.$plain_cols->[$i]"} = $obj->get_column($obj_cols->[$i]);
+          $ret->{join_free_condition}{"$plain_alias.$plain_cols->[$i]"} = $obj->get_column($obj_cols->[$i]);
         }
       }
-
-      return ($cond, 0); # joinfree
     }
   }
   elsif (ref $args->{condition} eq 'ARRAY') {
     if (@{$args->{condition}} == 0) {
-      return UNRESOLVABLE_CONDITION;
+      $ret = {
+        condition => UNRESOLVABLE_CONDITION,
+        join_free_condition => UNRESOLVABLE_CONDITION,
+      };
     }
     elsif (@{$args->{condition}} == 1) {
-      return $self->_resolve_relationship_condition({
+      $ret = $self->_resolve_relationship_condition({
         %$args,
         condition => $args->{condition}[0],
       });
     }
     else {
-      # FIXME - we are discarding nonvalues here... likely incorrect...
-      # then again - the entire thing is an OR, so we *can't* use
-      # the values anyway
-      # Return a hard crosstable => 1 to ensure nothing tries to use
-      # the result in such manner
-      my @ret;
-      for (@{$args->{condition}}) {
-        my ($cond) = $self->_resolve_relationship_condition({
-          %$args,
-          condition => $_,
-        });
-        push @ret, $cond;
+      # we are discarding inferred values here... likely incorrect...
+      # then again - the entire thing is an OR, so we *can't* use them anyway
+      for my $subcond ( map
+        { $self->_resolve_relationship_condition({ %$args, condition => $_ }) }
+        @{$args->{condition}}
+      ) {
+        $self->throw_exception('Either all or none of the OR-condition members can resolve to a join-free condition')
+          if $ret->{join_free_condition} and ! $subcond->{join_free_condition};
+
+        $subcond->{$_} and push @{$ret->{$_}}, $subcond->{$_} for (qw(condition join_free_condition));
       }
-      return (\@ret, 1); # forced cross-tab
     }
   }
   else {
     $self->throw_exception ("Can't handle condition $args->{condition} for relationship '$args->{rel_name}' yet :(");
   }
 
-  die "not supposed to get here - missing return()";
+  $self->throw_exception("Relationship '$args->{rel_name}' does not resolve to a join-free condition fragment") if (
+    $args->{require_join_free_condition}
+      and
+    ( ! $ret->{join_free_condition} or $ret->{join_free_condition} eq UNRESOLVABLE_CONDITION )
+  );
+
+  # we got something back - sanity check and infer values if we can
+  my @nonvalues;
+  if ( my $jfc = $ret->{join_free_condition} and $ret->{join_free_condition} ne UNRESOLVABLE_CONDITION ) {
+
+    my $jfc_eqs = $self->schema->storage->_extract_fixed_condition_columns($jfc, 'consider_nulls');
+
+    if (keys %$jfc_eqs) {
+
+      for (keys %$jfc) {
+        # $jfc is fully qualified by definition
+        my ($col) = $_ =~ /\.(.+)/;
+
+        if (exists $jfc_eqs->{$_} and ($jfc_eqs->{$_}||'') ne UNRESOLVABLE_CONDITION) {
+          $ret->{inferred_values}{$col} = $jfc_eqs->{$_};
+        }
+        elsif ( !$args->{infer_values_based_on} or ! exists $args->{infer_values_based_on}{$col} ) {
+          push @nonvalues, $col;
+        }
+      }
+
+      # all or nothing
+      delete $ret->{inferred_values} if @nonvalues;
+    }
+  }
+
+  # did the user explicitly ask
+  if ($args->{infer_values_based_on}) {
+
+    $self->throw_exception(sprintf (
+      "Unable to complete value inferrence - custom relationship '%s' returns conditions instead of values for column(s): %s",
+      $args->{rel_name},
+      map { "'$_'" } @nonvalues
+    )) if @nonvalues;
+
+
+    $ret->{inferred_values} ||= {};
+
+    $ret->{inferred_values}{$_} = $args->{infer_values_based_on}{$_}
+      for keys %{$args->{infer_values_based_on}};
+  }
+
+  $ret
 }
 
 =head2 related_source
index a623b4b..10ab27c 100644 (file)
@@ -159,7 +159,7 @@ lives_ok {
 # try to create_related a 80s cd
 throws_ok {
   $artist->create_related('cds_80s', { title => 'related creation 1' });
-} qr/\QCustom relationship 'cds_80s' not definitive - returns conditions instead of values for column(s): 'year'/,
+} qr/\QUnable to complete value inferrence - custom relationship 'cds_80s' returns conditions instead of values for column(s): 'year'/,
 'Create failed - complex cond';
 
 # now supply an explicit arg overwriting the ambiguous cond