Merge branch 'current/for_cpan_index' into current/dq
[dbsrgits/DBIx-Class.git] / lib / DBIx / Class / ResultSource.pm
index a4d3169..ca837ff 100644 (file)
@@ -3,18 +3,19 @@ package DBIx::Class::ResultSource;
 use strict;
 use warnings;
 
+use base qw/DBIx::Class::ResultSource::RowParser DBIx::Class/;
+
 use DBIx::Class::ResultSet;
 use DBIx::Class::ResultSourceHandle;
 
-use DBIx::Class::Exception;
 use DBIx::Class::Carp;
 use Devel::GlobalDestruction;
 use Try::Tiny;
 use List::Util 'first';
 use Scalar::Util qw/blessed weaken isweak/;
-use namespace::clean;
+use Data::Query::ExprHelpers;
 
-use base qw/DBIx::Class/;
+use namespace::clean;
 
 __PACKAGE__->mk_group_accessors(simple => qw/
   source_name name source_info
@@ -84,7 +85,7 @@ created, see L<DBIx::Class::ResultSource::View> for full details.
 =head2 Finding result source objects
 
 As mentioned above, a result source instance is created and stored for
-you when you define a L<Result Class|DBIx::Class::Manual::Glossary/Result Class>.
+you when you define a L<result class|DBIx::Class::Manual::Glossary/Result class>.
 
 You can retrieve the result source at runtime in the following ways:
 
@@ -96,7 +97,7 @@ You can retrieve the result source at runtime in the following ways:
 
 =item From a Result object:
 
-   $row->result_source;
+   $result->result_source;
 
 =item From a ResultSet object:
 
@@ -492,9 +493,9 @@ sub columns_info {
       }
       else {
         $self->throw_exception( sprintf (
-          "No such column '%s' on source %s",
+          "No such column '%s' on source '%s'",
           $_,
-          $self->source_name,
+          $self->source_name || $self->name || 'Unknown source...?',
         ));
       }
     }
@@ -588,11 +589,18 @@ for more info.
 
 sub set_primary_key {
   my ($self, @cols) = @_;
-  # check if primary key columns are valid columns
-  foreach my $col (@cols) {
-    $self->throw_exception("No such column $col on table " . $self->name)
-      unless $self->has_column($col);
+
+  my $colinfo = $self->columns_info(\@cols);
+  for my $col (@cols) {
+    carp_unique(sprintf (
+      "Primary key of source '%s' includes the column '%s' which has its "
+    . "'is_nullable' attribute set to true. This is a mistake and will cause "
+    . 'various Result-object operations to fail',
+      $self->source_name || $self->name || 'Unknown source...?',
+      $col,
+    )) if $colinfo->{$col}{is_nullable};
   }
+
   $self->_primaries(\@cols);
 
   $self->add_unique_constraint(primary => \@cols);
@@ -620,7 +628,7 @@ sub primary_columns {
 # a helper method that will automatically die with a descriptive message if
 # no pk is defined on the source in question. For internal use to save
 # on if @pks... boilerplate
-sub _pri_cols {
+sub _pri_cols_or_die {
   my $self = shift;
   my @pcols = $self->primary_columns
     or $self->throw_exception (sprintf(
@@ -631,6 +639,20 @@ sub _pri_cols {
   return @pcols;
 }
 
+# same as above but mandating single-column PK (used by relationship condition
+# inferrence)
+sub _single_pri_col_or_die {
+  my $self = shift;
+  my ($pri, @too_many) = $self->_pri_cols_or_die;
+
+  $self->throw_exception( sprintf(
+    "Operation requires a single-column primary key declared on '%s'",
+    $self->source_name || $self->result_class || $self->name || 'Unknown source...?',
+  )) if @too_many;
+  return $pri;
+}
+
+
 =head2 sequence
 
 Manually define the correct sequence for your table, to avoid the overhead
@@ -1034,9 +1056,9 @@ exists.
 
 =over 4
 
-=item Arguments: \%attrs
+=item Arguments: L<\%attrs|DBIx::Class::ResultSet/ATTRIBUTES>
 
-=item Return Value: \%attrs
+=item Return Value: L<\%attrs|DBIx::Class::ResultSet/ATTRIBUTES>
 
 =back
 
@@ -1047,8 +1069,35 @@ exists.
   $source->resultset_attributes({ order_by => [ 'id' ] });
 
 Store a collection of resultset attributes, that will be set on every
-L<DBIx::Class::ResultSet> produced from this result source. For a full
-list see L<DBIx::Class::ResultSet/ATTRIBUTES>.
+L<DBIx::Class::ResultSet> produced from this result source.
+
+B<CAVEAT>: C<resultset_attributes> comes with its own set of issues and
+bugs! While C<resultset_attributes> isn't deprecated per se, its usage is
+not recommended!
+
+Since relationships use attributes to link tables together, the "default"
+attributes you set may cause unpredictable and undesired behavior.  Furthermore,
+the defaults cannot be turned off, so you are stuck with them.
+
+In most cases, what you should actually be using are project-specific methods:
+
+  package My::Schema::ResultSet::Artist;
+  use base 'DBIx::Class::ResultSet';
+  ...
+
+  # BAD IDEA!
+  #__PACKAGE__->resultset_attributes({ prefetch => 'tracks' });
+
+  # GOOD IDEA!
+  sub with_tracks { shift->search({}, { prefetch => 'tracks' }) }
+
+  # in your code
+  $schema->resultset('Artist')->with_tracks->...
+
+This gives you the flexibility of not using it when you don't need it.
+
+For more complex situations, another solution would be to use a virtual view
+via L<DBIx::Class::ResultSource::View>.
 
 =cut
 
@@ -1340,8 +1389,8 @@ name. The keys/values are as specified for L<DBIx::Class::Relationship::Base/add
 =cut
 
 sub relationship_info {
-  my ($self, $rel) = @_;
-  return $self->_relationships->{$rel};
+  #my ($self, $rel) = @_;
+  return shift->_relationships->{+shift};
 }
 
 =head2 has_relationship
@@ -1359,8 +1408,8 @@ Returns true if the source has a relationship of this name, false otherwise.
 =cut
 
 sub has_relationship {
-  my ($self, $rel) = @_;
-  return exists $self->_relationships->{$rel};
+  #my ($self, $rel) = @_;
+  return exists shift->_relationships->{+shift};
 }
 
 =head2 reverse_relationship_info
@@ -1395,16 +1444,14 @@ sub reverse_relationship_info {
 
   my $ret = {};
 
-  return $ret unless ((ref $rel_info->{cond}) eq 'HASH');
-
   my $stripped_cond = $self->__strip_relcond ($rel_info->{cond});
 
-  my $rsrc_schema_moniker = $self->source_name
-    if try { $self->schema };
+  return $ret unless $stripped_cond;
+
+  my $registered_source_name = $self->source_name;
 
   # this may be a partial schema or something else equally esoteric
-  my $other_rsrc = try { $self->related_source($rel) }
-    or return $ret;
+  my $other_rsrc = $self->related_source($rel);
 
   # Get all the relationships for that source that related to this source
   # whose foreign column set are our self columns on $rel and whose self
@@ -1419,11 +1466,11 @@ sub reverse_relationship_info {
     my $roundtrip_rsrc = try { $other_rsrc->related_source($other_rel) }
       or next;
 
-    if ($rsrc_schema_moniker and try { $roundtrip_rsrc->schema } ) {
-      next unless $rsrc_schema_moniker eq $roundtrip_rsrc->source_name;
+    if ($registered_source_name) {
+      next if $registered_source_name ne ($roundtrip_rsrc->source_name || '')
     }
     else {
-      next unless $self->result_class eq $roundtrip_rsrc->result_class;
+      next if $self->result_class ne $roundtrip_rsrc->result_class;
     }
 
     my $other_rel_info = $other_rsrc->relationship_info($other_rel);
@@ -1431,9 +1478,10 @@ sub reverse_relationship_info {
     # this can happen when we have a self-referential class
     next if $other_rel_info eq $rel_info;
 
-    next unless ref $other_rel_info->{cond} eq 'HASH';
     my $other_stripped_cond = $self->__strip_relcond($other_rel_info->{cond});
 
+    next unless $other_stripped_cond;
+
     $ret->{$other_rel} = $other_rel_info if (
       $self->_compare_relationship_keys (
         [ keys %$stripped_cond ], [ values %$other_stripped_cond ]
@@ -1448,19 +1496,110 @@ sub reverse_relationship_info {
   return $ret;
 }
 
+sub _join_condition_to_hashref {
+  my ($self, $dq) = @_;
+  my (@q, %found) = ($dq);
+  Q: while (my $n = shift @q) {
+    if (is_Operator($n)) {
+      if (($n->{operator}{Perl}||'') =~ /^(?:==|eq)$/) {
+        my ($l, $r) = @{$n->{args}};
+        if (
+          is_Identifier($l) and @{$l->{elements}} == 2
+          and is_Identifier($r) and @{$r->{elements}} == 2
+        ) {
+          ($l, $r) = ($r, $l) if $l->{elements}[0] eq 'self';
+          if (
+            $l->{elements}[0] eq 'foreign'
+            and $r->{elements}[0] eq 'self'
+          ) {
+            $found{$l->{elements}[1]} = $r->{elements}[1];
+            next Q;
+          }
+        }
+      } elsif (($n->{operator}{Perl}||'') eq 'and') {
+        push @q, @{$n->{args}};
+        next Q;
+      }
+    }
+    # didn't match as 'and' or 'foreign.x = self.y', can't handle this
+    return undef;
+  }
+  return keys %found ? \%found : undef;
+}
+
 # all this does is removes the foreign/self prefix from a condition
 sub __strip_relcond {
-  +{
-    map
-      { map { /^ (?:foreign|self) \. (\w+) $/x } ($_, $_[1]{$_}) }
-      keys %{$_[1]}
+  if (ref($_[1]) eq 'HASH') {
+    return +{
+      map
+        { map { /^ (?:foreign|self) \. (\w+) $/x } ($_, $_[1]{$_}) }
+        keys %{$_[1]}
+    };
+  } elsif (blessed($_[1]) and $_[1]->isa('Data::Query::ExprBuilder')) {
+    return $_[0]->_join_condition_to_hashref($_[1]->{expr});
   }
+  return undef;
 }
 
-sub compare_relationship_keys {
-  carp 'compare_relationship_keys is a private method, stop calling it';
-  my $self = shift;
-  $self->_compare_relationship_keys (@_);
+sub _extract_fixed_values_for {
+  my ($self, $dq, $alias) = @_;
+  my $fixed = $self->_extract_fixed_conditions_for($dq, $alias);
+  return +{ map {
+    is_Value($fixed->{$_})
+      ? ($_ => $fixed->{$_}{value})
+      : (is_Literal($fixed->{$_}) ? ($_ => \($fixed->{$_})) : ())
+  } keys %$fixed };
+}
+
+sub _extract_fixed_conditions_for {
+  my ($self, $dq, $alias) = @_;
+  my (@q, %found) = ($dq);
+  foreach my $n ($self->_extract_top_level_conditions($dq)) {
+    if (
+      is_Operator($n)
+      and (
+        ($n->{operator}{Perl}||'') =~ /^(?:==|eq)$/
+        or ($n->{operator}{'SQL.Naive'}||'') eq '='
+     )
+    ) {
+      my ($l, $r) = @{$n->{args}};
+      if (
+        is_Identifier($r) and (
+          !$alias
+          or (@{$r->{elements}} == 2
+              and $r->{elements}[0] eq $alias)
+        )
+      ) {
+        ($l, $r) = ($r, $l);
+      }
+      if (
+        is_Identifier($l) and (
+          !$alias
+          or (@{$l->{elements}} == 2
+              and $l->{elements}[0] eq $alias)
+        )
+      ) {
+        $found{$alias ? $l->{elements}[1] : join('.',@{$l->{elements}})} = $r;
+      }
+    }
+  }
+  return \%found;
+}
+
+sub _extract_top_level_conditions {
+  my ($self, $dq) = @_;
+  my (@q, @found) = ($dq);
+  while (my $n = shift @q) {
+    if (
+      is_Operator($n)
+      and ($n->{operator}{Perl}||$n->{operator}{'SQL.Naive'}||'') =~ /^and$/i
+    ) {
+      push @q, @{$n->{args}};
+    } else {
+      push @found, $n;
+    }
+  }
+  return @found;
 }
 
 # Returns true if both sets of keynames are the same, false otherwise.
@@ -1568,24 +1707,18 @@ sub _resolve_join {
                 ,
                -join_path => [@$jpath, { $join => $as } ],
                -is_single => (
-                  $rel_info->{attrs}{accessor}
-                    &&
+                  (! $rel_info->{attrs}{accessor})
+                    or
                   first { $rel_info->{attrs}{accessor} eq $_ } (qw/single filter/)
                 ),
                -alias => $as,
-               -relation_chain_depth => $seen->{-relation_chain_depth} || 0,
+               -relation_chain_depth => ( $seen->{-relation_chain_depth} || 0 ) + 1,
              },
              scalar $self->_resolve_condition($rel_info->{cond}, $as, $alias, $join)
           ];
   }
 }
 
-sub pk_depends_on {
-  carp 'pk_depends_on is a private method, stop calling it';
-  my $self = shift;
-  $self->_pk_depends_on (@_);
-}
-
 # Determines whether a relation is dependent on an object from this source
 # having already been inserted. Takes the name of the relationship and a
 # hashref of columns of the related object.
@@ -1599,10 +1732,19 @@ sub _pk_depends_on {
     if exists ($relinfo->{attrs}{is_foreign_key_constraint});
 
   my $cond = $relinfo->{cond};
-  return 0 unless ref($cond) eq 'HASH';
-
-  # map { foreign.foo => 'self.bar' } to { bar => 'foo' }
-  my $keyhash = { map { my $x = $_; $x =~ s/.*\.//; $x; } reverse %$cond };
+  my $keyhash = do {
+    if (ref($cond) eq 'HASH') {
+
+      # map { foreign.foo => 'self.bar' } to { bar => 'foo' }
+      +{ map { my $x = $_; $x =~ s/.*\.//; $x; } reverse %$cond };
+    } elsif (ref($cond) eq 'REF' and ref($$cond) eq 'HASH') {
+      my $fixed = $self->_join_condition_to_hashref($$cond);
+      return 0 unless $fixed;
+      +{ reverse %$fixed };
+    } else {
+      return 0;
+    }
+  };
 
   # assume anything that references our PK probably is dependent on us
   # rather than vice versa, unless the far side is (a) defined or (b)
@@ -1622,22 +1764,18 @@ sub _pk_depends_on {
   return 1;
 }
 
-sub resolve_condition {
-  carp 'resolve_condition is a private method, stop calling it';
-  my $self = shift;
-  $self->_resolve_condition (@_);
-}
+our $UNRESOLVABLE_CONDITION = \Literal(SQL => '1 = 0');
 
-our $UNRESOLVABLE_CONDITION = \ '1 = 0';
+${$UNRESOLVABLE_CONDITION}->{'DBIx::Class::ResultSource.UNRESOLVABLE'} = 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-triviail values (notmally conditions) returned as a part
+# list of non-trivial values (normally conditions) returned as a part
 # of a joinfree condition hash
 sub _resolve_condition {
   my ($self, $cond, $as, $for, $rel_name) = @_;
 
-  my $obj_rel = !!blessed $for;
+  my $obj_rel = defined blessed $for;
 
   if (ref $cond eq 'CODE') {
     my $relalias = $obj_rel ? 'me' : $as;
@@ -1765,115 +1903,75 @@ sub _resolve_condition {
     }
     return wantarray ? (\@ret, $crosstable) : \@ret;
   }
-  else {
-    $self->throw_exception ("Can't handle condition $cond for relationship '$rel_name' yet :(");
-  }
-}
-
-# Accepts one or more relationships for the current source and returns an
-# array of column names for each of those relationships. Column names are
-# prefixed relative to the current source, in accordance with where they appear
-# in the supplied relationships.
-sub _resolve_prefetch {
-  my ($self, $pre, $alias, $alias_map, $order, $collapse, $pref_path) = @_;
-  $pref_path ||= [];
-
-  if (not defined $pre or not length $pre) {
-    return ();
-  }
-  elsif( ref $pre eq 'ARRAY' ) {
-    return
-      map { $self->_resolve_prefetch( $_, $alias, $alias_map, $order, $collapse, [ @$pref_path ] ) }
-        @$pre;
-  }
-  elsif( ref $pre eq 'HASH' ) {
-    my @ret =
-    map {
-      $self->_resolve_prefetch($_, $alias, $alias_map, $order, $collapse, [ @$pref_path ] ),
-      $self->related_source($_)->_resolve_prefetch(
-               $pre->{$_}, "${alias}.$_", $alias_map, $order, $collapse, [ @$pref_path, $_] )
-    } keys %$pre;
-    return @ret;
-  }
-  elsif( ref $pre ) {
-    $self->throw_exception(
-      "don't know how to resolve prefetch reftype ".ref($pre));
-  }
-  else {
-    my $p = $alias_map;
-    $p = $p->{$_} for (@$pref_path, $pre);
-
-    $self->throw_exception (
-      "Unable to resolve prefetch '$pre' - join alias map does not contain an entry for path: "
-      . join (' -> ', @$pref_path, $pre)
-    ) if (ref $p->{-join_aliases} ne 'ARRAY' or not @{$p->{-join_aliases}} );
-
-    my $as = shift @{$p->{-join_aliases}};
-
-    my $rel_info = $self->relationship_info( $pre );
-    $self->throw_exception( $self->source_name . " has no such relationship '$pre'" )
-      unless $rel_info;
-    my $as_prefix = ($alias =~ /^.*?\.(.+)$/ ? $1.'.' : '');
-    my $rel_source = $self->related_source($pre);
-
-    if ($rel_info->{attrs}{accessor} && $rel_info->{attrs}{accessor} eq 'multi') {
-      $self->throw_exception(
-        "Can't prefetch has_many ${pre} (join cond too complex)")
-        unless ref($rel_info->{cond}) eq 'HASH';
-      my $dots = @{[$as_prefix =~ m/\./g]} + 1; # +1 to match the ".${as_prefix}"
-
-      if (my ($fail) = grep { @{[$_ =~ m/\./g]} == $dots }
-                         keys %{$collapse}) {
-        my ($last) = ($fail =~ /([^\.]+)$/);
-        carp (
-          "Prefetching multiple has_many rels ${last} and ${pre} "
-          .(length($as_prefix)
-            ? "at the same level (${as_prefix}) "
-            : "at top level "
-          )
-          . 'will explode the number of row objects retrievable via ->next or ->all. '
-          . 'Use at your own risk.'
-        );
-      }
-
-      #my @col = map { (/^self\.(.+)$/ ? ("${as_prefix}.$1") : ()); }
-      #              values %{$rel_info->{cond}};
-      $collapse->{".${as_prefix}${pre}"} = [ $rel_source->_pri_cols ];
-        # action at a distance. prepending the '.' allows simpler code
-        # in ResultSet->_collapse_result
-      my @key = map { (/^foreign\.(.+)$/ ? ($1) : ()); }
-                    keys %{$rel_info->{cond}};
-      push @$order, map { "${as}.$_" } @key;
-
-      if (my $rel_order = $rel_info->{attrs}{order_by}) {
-        # this is kludgy and incomplete, I am well aware
-        # but the parent method is going away entirely anyway
-        # so sod it
-        my $sql_maker = $self->storage->sql_maker;
-        my ($orig_ql, $orig_qr) = $sql_maker->_quote_chars;
-        my $sep = $sql_maker->name_sep;
-
-        # install our own quoter, so we can catch unqualified stuff
-        local $sql_maker->{quote_char} = ["\x00", "\xFF"];
-
-        my $quoted_prefix = "\x00${as}\xFF";
-
-        for my $chunk ( $sql_maker->_order_by_chunks ($rel_order) ) {
-          my @bind;
-          ($chunk, @bind) = @$chunk if ref $chunk;
-
-          $chunk = "${quoted_prefix}${sep}${chunk}"
-            unless $chunk =~ /\Q$sep/;
-
-          $chunk =~ s/\x00/$orig_ql/g;
-          $chunk =~ s/\xFF/$orig_qr/g;
-          push @$order, \[$chunk, @bind];
+  elsif (blessed($cond) and $cond->isa('Data::Query::ExprBuilder')) {
+    my (%cross, $unresolvable);
+    my $as = blessed($for) ? 'me' : $as;
+    my %action = map {
+      my ($ident, $thing, $other) = @$_;
+      ($ident => do {
+        if ($thing and !ref($thing)) {
+          sub {
+            $cross{$thing} = 1;
+            return \Identifier($thing, $_[0]->{elements}[1]);
+          }
+        } elsif (!defined($thing)) {
+          sub {
+            \perl_scalar_value(
+              undef,
+              $_[1] ? join('.', $other, $_[1]->{elements}[1]) : ()
+            );
+          }
+        } elsif ((ref($thing)||'') eq 'HASH') {
+          sub {
+            \perl_scalar_value(
+              $thing->{$_->{elements}[1]},
+              $_[1] ? join('.', $other, $_[1]->{elements}[1]) : ()
+            );
+          }
+        } elsif (blessed($thing)) {
+          sub {
+            unless ($thing->has_column_loaded($_[0]->{elements}[1])) {
+              if ($thing->in_storage) {
+                $self->throw_exception(sprintf
+                  "Unable to resolve relationship '%s' from object %s: column '%s' not "
+                . 'loaded from storage (or not passed to new() prior to insert()). You '
+                . 'probably need to call ->discard_changes to get the server-side defaults '
+                . 'from the database.',
+                  $as,
+                  $thing,
+                  $_[0]->{elements}[1]
+                );
+              }
+              $unresolvable = 1;
+            }
+            return \perl_scalar_value(
+                      $thing->get_column($_[0]->{elements}[1]),
+                      $_[1] ? join('.', $other, $_[1]->{elements}[1]) : ()
+                    );
+          }
+        } else {
+            die "I have no idea what ${thing} is supposed to be";
         }
+      })
+    } ([ foreign => $as, $for ], [ self => $for, $as ]);
+    my %seen;
+    my $mapped = map_dq_tree {
+      if (is_Operator and @{$_->{args}} == 2) {
+        @seen{@{$_->{args}}} = reverse @{$_->{args}};
       }
-    }
-
-    return map { [ "${as}.$_", "${as_prefix}${pre}.$_", ] }
-      $rel_source->columns;
+      if (
+        is_Identifier and @{$_->{elements}} == 2
+        and my $act = $action{$_->{elements}[0]}
+      ) {
+        return $act->($_, $seen{$_});
+      }
+      return $_;
+    } $cond->{expr};
+    return $UNRESOLVABLE_CONDITION if $unresolvable;
+    return (wantarray ? (\$mapped, (keys %cross == 2)) : \$mapped);
+  }
+  else {
+    $self->throw_exception ("Can't handle condition $cond for relationship '$rel_name' yet :(");
   }
 }