Merge branch 0.08200_track into master
Peter Rabbitson [Thu, 7 Apr 2011 23:55:41 +0000 (01:55 +0200)]
1  2 
Changes
lib/DBIx/Class.pm
lib/DBIx/Class/Relationship/HasOne.pm
lib/DBIx/Class/Relationship/ManyToMany.pm
lib/DBIx/Class/ResultSet.pm
lib/DBIx/Class/ResultSource.pm
lib/DBIx/Class/Row.pm
lib/DBIx/Class/SQLMaker.pm
lib/DBIx/Class/SQLMaker/OracleJoins.pm
t/relationship/custom.t

diff --combined Changes
+++ b/Changes
@@@ -1,59 -1,11 +1,65 @@@
  Revision history for DBIx::Class
  
 +    * New Features / Changes
 +        - Add quote_names connection option. When set to true automatically
 +          sets quote_char and name_sep appropriate for your RDBMS
 +        - Support for MS Access databases via DBD::ODBC and DBD::ADO (only
 +          Win32 support currently tested)
 +        - IC::DateTime support for MSSQL over DBD::ADO
 +        - Both the ::ODBC and ::ADO dispatchers now warn if a rdbms-specific
 +          driver is not found for this connection before falling back to
 +          plain ::Storage::DBI
 +        - ::Storage::DBI::sth was mistakenly marked/documented as public,
 +          privatize and warn on deprecated use
 +        - Massive overhaul of bind values/attributes handling - slightly
 +          changes the output of as_query (should not cause compat issues)
 +        - Support ancient DB2 versions (5.4 and older), with proper limit
 +          dialect
 +
 +    * Fixes
 +        - Fix ::Storage::DBI::* MRO problems on 5.8.x perls
 +        - Disable mysql_auto_reconnect for MySQL - depending on the ENV
 +          it sometimes defaults to on and causes major borkage on older
 +          DBD::mysql versions
 +        - Fix dropped bind values in select/group_by on Oracle (omission
 +          from 0542ec57 and 4c2b30d6)
 +        - Fix remaining errors with Oracle and identifiers longer than the
 +          Oracle-imposed maximum of 30 characters (RT#66390)
 +        - Fix older oracle-specific "WhereJoins" to work properly with
 +          name quoting
 +        - Fix problems with M.A.D. under CGI::SpeedyCGI (RT#65131)
 +        - Better error handling when prepare() fails silently
 +        - Fixes skipped lines when a comment is followed by a statement
 +          when deploying a schema via sql file
 +        - Fix reverse_relationship_info on prototypical result sources
 +          (sources not yet registered with a schema)
 +        - Warn and skip relationships missing from a partial schema during
 +          dbic cascade_delete
 +        - Automatically require the requested cursor class before use
 +          (RT#64795)
 +        - Work around a Firebird ODBC driver bug exposed by DBD::ODBC 1.29
 +        - Fix exiting via next warnings in ResultSource::sequence()
 +        - Fix stripping of table qualifiers in update/delete in arrayref
 +          condition elements
 +        - Change SQLMaker carp-monkeypatch to be compatible with versions
 +          of SQL::Abstract >= 1.73
 +        - Fix using \[] literals in the from resultset attribute
 +        - Fix populate() with \[], arrays (datatype) and other exotic values
 +
 +    * Misc
 +        - Rewire all warnings to a new Carp-like implementation internal
 +          to DBIx::Class, and remove the Carp::Clan dependency
 +        - Only load Class::C3 and friends if necessary ($] < 5.010)
 +        - Greatly reduced loading of non-essential modules to aid startup
 +          time (mainly benefiting CGI users)
 +        - Make sure all namespaces are clean of rogue imports
 +
+ 0.08190-TRIAL 2011-01-24 15:35 (UTC)
+     * New Features / Changes
+         - Support for completely arbitrary SQL::Abstract-based conditions
+           in all types of relationships
  0.08127 2011-01-19 16:40 (UTC)
      * New Features / Changes
          - Schema/resultsource instances are now crossreferenced via a new
diff --combined lib/DBIx/Class.pm
@@@ -3,57 -3,15 +3,57 @@@ package DBIx::Class
  use strict;
  use warnings;
  
 -use MRO::Compat;
 +BEGIN {
 +  package DBIx::Class::_ENV_;
 +
 +  if ($] < 5.009_005) {
 +    require MRO::Compat;
 +    *OLD_MRO = sub () { 1 };
 +  }
 +  else {
 +    require mro;
 +    *OLD_MRO = sub () { 0 };
 +  }
 +
 +  # ::Runmode would only be loaded by DBICTest, which in turn implies t/
 +  *DBICTEST = eval { DBICTest::RunMode->is_author }
 +    ? sub () { 1 }
 +    : sub () { 0 }
 +  ;
 +
 +  # During 5.13 dev cycle HELEMs started to leak on copy
 +  *PEEPEENESS = (defined $ENV{DBICTEST_ALL_LEAKS}
 +    # request for all tests would force "non-leaky" illusion and vice-versa
 +    ? ! $ENV{DBICTEST_ALL_LEAKS}
 +
 +    # otherwise confess that this perl is busted ONLY on smokers
 +    : do {
 +      if (eval { DBICTest::RunMode->is_smoker }) {
 +
 +        # leaky 5.13.6 (fixed in blead/cefd5c7c)
 +        if ($] == '5.013006') { 1 }
 +
 +        # not sure why this one leaks, but disable anyway - ANDK seems to make it weep
 +        elsif ($] == '5.013005') { 1 }
 +
 +        else { 0 }
 +      }
 +      else { 0 }
 +    }
 +  ) ? sub () { 1 } : sub () { 0 };
 +}
 +
  use mro 'c3';
  
  use DBIx::Class::Optional::Dependencies;
  
  use vars qw($VERSION);
 -use base qw/DBIx::Class::Componentised Class::Accessor::Grouped/;
 +use base qw/DBIx::Class::Componentised DBIx::Class::AccessorGroup/;
  use DBIx::Class::StartupCheck;
  
 +__PACKAGE__->mk_group_accessors(inherited => '_skip_namespace_frames');
 +__PACKAGE__->_skip_namespace_frames('^DBIx::Class|^SQL::Abstract|^Try::Tiny');
 +
  sub mk_classdata {
    shift->mk_classaccessor(@_);
  }
@@@ -69,7 -27,7 +69,7 @@@ sub component_base_class { 'DBIx::Class
  # Always remember to do all digits for the version even if they're 0
  # i.e. first release of 0.XX *must* be 0.XX000. This avoids fBSD ports
  # brain damage and presumably various other packaging systems too
- $VERSION = '0.08127';
+ $VERSION = '0.08190';
  
  $VERSION = eval $VERSION if $VERSION =~ /_/; # numify for warning-free dev releases
  
@@@ -340,8 -298,6 +340,8 @@@ goraxe: Gordon Irving <goraxe@cpan.org
  
  gphat: Cory G Watson <gphat@cpan.org>
  
 +Grant Street Group L<http://www.grantstreet.com/>
 +
  groditi: Guillermo Roditi <groditi@cpan.org>
  
  Haarg: Graham Knop <haarg@haarg.org>
@@@ -352,8 -308,6 +352,8 @@@ ilmari: Dagfinn Ilmari MannsE<aring>ke
  
  initself: Mike Baas <mike@initselftech.com>
  
 +jawnsy: Jonathan Yu <jawnsy@cpan.org>
 +
  jasonmay: Jason May <jason.a.may@gmail.com>
  
  jesper: Jesper Krogh
@@@ -382,12 -336,8 +382,12 @@@ marcus: Marcus Ramberg <mramberg@cpan.o
  
  mattlaw: Matt Lawrence
  
 +mattp: Matt Phillips <mattp@cpan.org>
 +
  michaelr: Michael Reddick <michael.reddick@gmail.com>
  
 +milki: Jonathan Chu <milki@rescomp.berkeley.edu>
 +
  ned: Neil de Carteret
  
  nigel: Nigel Metheringham <nigelm@cpan.org>
@@@ -438,8 -388,6 +438,8 @@@ rjbs: Ricardo Signes <rjbs@cpan.org
  
  robkinyon: Rob Kinyon <rkinyon@cpan.org>
  
 +Robert Olson <bob@rdolson.org>
 +
  Roman: Roman Filippov <romanf@cpan.org>
  
  Sadrak: Felix Antonius Wilhelm Ostmann <sadrak@cpan.org>
@@@ -460,8 -408,6 +460,8 @@@ sszabo: Stephan Szabo <sszabo@bigpanda.
  
  talexb: Alex Beamish <talexb@gmail.com>
  
 +tamias: Ronald J Kimball <rjk@tamias.net>
 +
  teejay : Aaron Trevena <teejay@cpan.org>
  
  Todd Lipcon
@@@ -3,7 -3,7 +3,7 @@@ package # hide from PAUS
  
  use strict;
  use warnings;
 -use Carp::Clan qw/^DBIx::Class/;
 +use DBIx::Class::Carp;
  use Try::Tiny;
  use namespace::clean;
  
@@@ -50,10 -50,14 +50,14 @@@ sub _has_one 
      $cond = { "foreign.${f_key}" => "self.${pri}" };
    }
    $class->_validate_has_one_condition($cond);
+   my $default_cascade = ref $cond eq 'CODE' ? 0 : 1;
    $class->add_relationship($rel, $f_class,
     $cond,
     { accessor => 'single',
-      cascade_update => 1, cascade_delete => 1,
+      cascade_update => $default_cascade,
+      cascade_delete => $default_cascade,
       ($join_type ? ('join_type' => $join_type) : ()),
       %{$attrs || {}} });
    1;
@@@ -4,7 -4,7 +4,7 @@@ package # hide from PAUS
  use strict;
  use warnings;
  
 -use Carp::Clan qw/^DBIx::Class/;
 +use DBIx::Class::Carp;
  use Sub::Name qw/subname/;
  use Scalar::Util qw/blessed/;
  
@@@ -133,9 -133,15 +133,15 @@@ EO
          unless blessed ($obj);
        my $rel_source = $self->search_related($rel)->result_source;
        my $cond = $rel_source->relationship_info($f_rel)->{cond};
-       my $link_cond = $rel_source->_resolve_condition(
-         $cond, $obj, $f_rel
+       my ($link_cond, $crosstable) = $rel_source->_resolve_condition(
+         $cond, $obj, $f_rel, $f_rel
        );
+       $self->throw_exception(
+         "Custom relationship '$rel' does not resolve to a join-free condition, "
+        ."unable to use with the ManyToMany helper '$f_rel'"
+       ) if $crosstable;
        $self->search_related($rel, $link_cond)->delete;
      };
  
@@@ -3,23 -3,28 +3,23 @@@ package DBIx::Class::ResultSet
  use strict;
  use warnings;
  use base qw/DBIx::Class/;
 -use Carp::Clan qw/^DBIx::Class/;
 +use DBIx::Class::Carp;
  use DBIx::Class::Exception;
 -use Data::Page;
  use DBIx::Class::ResultSetColumn;
 -use DBIx::Class::ResultSourceHandle;
 -use Hash::Merge ();
  use Scalar::Util qw/blessed weaken/;
  use Try::Tiny;
 -use Storable qw/nfreeze thaw/;
  
  # not importing first() as it will clash with our own method
  use List::Util ();
  
 -use namespace::clean;
 -
 -
  BEGIN {
    # De-duplication in _merge_attr() is disabled, but left in for reference
    # (the merger is used for other things that ought not to be de-duped)
    *__HM_DEDUP = sub () { 0 };
  }
  
 +use namespace::clean;
 +
  use overload
          '0+'     => "count",
          'bool'   => "_bool",
@@@ -93,7 -98,7 +93,7 @@@ another
        year => $request->param('year'),
      });
  
 -    $self->apply_security_policy( $cd_rs );
 +    $cd_rs = $self->apply_security_policy( $cd_rs );
  
      return $cd_rs->all();
    }
@@@ -296,6 -301,7 +296,6 @@@ always return a resultset, even in lis
  
  =cut
  
 -my $callsites_warned;
  sub search_rs {
    my $self = shift;
  
    } if @_;
  
    if( @_ > 1 and ! $rsrc->result_class->isa('DBIx::Class::CDBICompat') ) {
 -    # determine callsite obeying Carp::Clan rules (fucking ugly but don't have better ideas)
 -    my $callsite = do {
 -      my $w;
 -      local $SIG{__WARN__} = sub { $w = shift };
 -      carp;
 -      $w
 -    };
 -    carp 'search( %condition ) is deprecated, use search( \%condition ) instead'
 -      unless $callsites_warned->{$callsite}++;
 +    carp_unique 'search( %condition ) is deprecated, use search( \%condition ) instead';
    }
  
    for ($old_where, $call_cond) {
@@@ -683,7 -697,7 +683,7 @@@ sub find 
        next if $keyref eq 'ARRAY'; # has_many for multi_create
  
        my $rel_q = $rsrc->_resolve_condition(
-         $relinfo->{cond}, $val, $key
+         $relinfo->{cond}, $val, $key, $key
        );
        die "Can't handle complex relationship conditions in find" if ref($rel_q) ne 'HASH';
        @related{keys %$rel_q} = values %$rel_q;
@@@ -783,6 -797,7 +783,6 @@@ sub _qualify_cond_columns 
    return \%aliased;
  }
  
 -my $callsites_warned_ucond;
  sub _build_unique_cond {
    my ($self, $constraint_name, $extra_cond, $croak_on_null) = @_;
  
        and
      my @undefs = grep { ! defined $final_cond->{$_} } (keys %$final_cond)
    ) {
 -    my $callsite = do {
 -      my $w;
 -      local $SIG{__WARN__} = sub { $w = shift };
 -      carp;
 -      $w
 -    };
 -
 -    carp ( sprintf (
 +    carp_unique ( sprintf (
        "NULL/undef values supplied for requested unique constraint '%s' (NULL "
      . 'values in column(s): %s). This is almost certainly not what you wanted, '
      . 'though you can set DBIC_NULLABLE_KEY_NOWARN to disable this warning.',
        $constraint_name,
        join (', ', map { "'$_'" } @undefs),
 -    )) unless $callsites_warned_ucond->{$callsite}++;
 +    ));
    }
  
    return $final_cond;
@@@ -1054,7 -1076,7 +1054,7 @@@ instead. An example conversion is
  
  sub search_like {
    my $class = shift;
 -  carp (
 +  carp_unique (
      'search_like() is deprecated and will be removed in DBIC version 0.09.'
     .' Instead use ->search({ x => { -like => "y%" } })'
     .' (note the outer pair of {}s - they are important!)'
@@@ -1952,6 -1974,7 +1952,7 @@@ sub populate 
            $reverse_relinfo->{cond},
            $self,
            $result,
+           $rel,
          );
  
          delete $data->[$index]->{$rel};
            $rels->{$rel}{cond},
            $child,
            $main_row,
+           $rel,
          );
  
          my @rows_to_add = ref $item->{$rel} eq 'ARRAY' ? @{$item->{$rel}} : ($item->{$rel});
@@@ -2172,7 -2196,6 +2174,7 @@@ sub pager 
  ### necessary for future development of DBIx::DS. Do *NOT* change this code
  ### before talking to ribasushi/mst
  
 +  require Data::Page;
    my $pager = Data::Page->new(
      0,  #start with an empty set
      $attrs->{rows},
@@@ -2322,7 -2345,13 +2324,13 @@@ sub _merge_with_rscond 
  
      while ( my($col, $value) = each %implied ) {
        my $vref = ref $value;
-       if ($vref eq 'HASH' && keys(%$value) && (keys %$value)[0] eq '=') {
+       if (
+         $vref eq 'HASH'
+           and
+         keys(%$value) == 1
+           and
+         (keys %$value)[0] eq '='
+       ) {
          $new_data{$col} = $value->{'='};
        }
        elsif( !$vref or $vref eq 'SCALAR' or blessed($value) ) {
@@@ -3345,7 -3374,7 +3353,7 @@@ sub _resolved_attrs 
    # subquery (since a group_by is present)
    if (delete $attrs->{distinct}) {
      if ($attrs->{group_by}) {
 -      carp ("Useless use of distinct on a grouped resultset ('distinct' is ignored when a 'group_by' is present)");
 +      carp_unique ("Useless use of distinct on a grouped resultset ('distinct' is ignored when a 'group_by' is present)");
      }
      else {
        # distinct affects only the main selection part, not what prefetch may
@@@ -3537,7 -3566,6 +3545,7 @@@ sub _merge_joinpref_attr 
  
    sub _merge_attr {
      $hm ||= do {
 +      require Hash::Merge;
        my $hm = Hash::Merge->new;
  
        $hm->specify_behavior({
@@@ -3627,14 -3655,14 +3635,14 @@@ sub STORABLE_freeze 
    # A cursor in progress can't be serialized (and would make little sense anyway)
    delete $to_serialize->{cursor};
  
 -  nfreeze($to_serialize);
 +  Storable::nfreeze($to_serialize);
  }
  
  # need this hook for symmetry
  sub STORABLE_thaw {
    my ($self, $cloning, $serialized) = @_;
  
 -  %$self = %{ thaw($serialized) };
 +  %$self = %{ Storable::thaw($serialized) };
  
    $self;
  }
@@@ -3746,10 -3774,6 +3754,10 @@@ passed to object inflation. Note that t
  column (or relationship) accessor, and 'name' is the name of the column
  accessor in the related table.
  
 +B<NOTE:> You need to explicitly quote '+columns' when defining the attribute.
 +Not doing so causes Perl to incorrectly interpret +columns as a bareword with a
 +unary plus operator before it.
 +
  =head2 include_columns
  
  =over 4
@@@ -3790,10 -3814,6 +3798,10 @@@ identifier aliasing. You can however al
  e.g. an C<ORDER BY> clause. This is done via the C<-as> B<select function
  attribute> supplied as shown in the example above.
  
 +B<NOTE:> You need to explicitly quote '+select'/'+as' when defining the attributes.
 +Not doing so causes Perl to incorrectly interpret them as a bareword with a
 +unary plus operator before it.
 +
  =head2 +select
  
  =over 4
@@@ -4026,7 -4046,7 +4034,7 @@@ Makes the resultset paged and specifie
  identical to creating a non-pages resultset and then calling ->page($page)
  on it.
  
 -If L<rows> attribute is not specified it defaults to 10 rows per page.
 +If L</rows> attribute is not specified it defaults to 10 rows per page.
  
  When you have a paged resultset, L</count> will only return the number
  of rows in the page. To get the total, use the L</pager> and call
@@@ -7,10 -7,11 +7,10 @@@ use DBIx::Class::ResultSet
  use DBIx::Class::ResultSourceHandle;
  
  use DBIx::Class::Exception;
 -use Carp::Clan qw/^DBIx::Class/;
 +use DBIx::Class::Carp;
  use Try::Tiny;
  use List::Util 'first';
- use Scalar::Util qw/weaken isweak/;
+ use Scalar::Util qw/blessed weaken isweak/;
 -use Storable qw/nfreeze thaw/;
  use namespace::clean;
  
  use base qw/DBIx::Class/;
@@@ -427,7 -428,7 +427,7 @@@ sub columns 
    my $columns_info = $source->columns_info;
  
  Like L</column_info> but returns information for the requested columns. If
 -the optional column-list arrayref is ommitted it returns info on all columns
 +the optional column-list arrayref is omitted it returns info on all columns
  currently defined on the ResultSource via L</add_columns>.
  
  =cut
@@@ -633,7 -634,7 +633,7 @@@ sub sequence 
    my ($self,$seq) = @_;
  
    my @pks = $self->primary_columns
 -    or next;
 +    or return;
  
    $_->{sequence} = $seq
      for values %{ $self->columns_info (\@pks) };
@@@ -1326,74 -1327,56 +1326,74 @@@ L</relationship_info>
  
  sub reverse_relationship_info {
    my ($self, $rel) = @_;
 -  my $rel_info = $self->relationship_info($rel);
 +
 +  my $rel_info = $self->relationship_info($rel)
 +    or $self->throw_exception("No such relationship '$rel'");
 +
    my $ret = {};
  
    return $ret unless ((ref $rel_info->{cond}) eq 'HASH');
  
 -  my @cond = keys(%{$rel_info->{cond}});
 -  my @refkeys = map {/^\w+\.(\w+)$/} @cond;
 -  my @keys = map {$rel_info->{cond}->{$_} =~ /^\w+\.(\w+)$/} @cond;
 +  my $stripped_cond = $self->__strip_relcond ($rel_info->{cond});
 +
 +  my $rsrc_schema_moniker = $self->source_name
 +    if try { $self->schema };
  
 -  # Get the related result source for this relationship
 -  my $othertable = $self->related_source($rel);
 +  # this may be a partial schema or something else equally esoteric
 +  my $other_rsrc = try { $self->related_source($rel) }
 +    or return $ret;
  
    # 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
 -  # columns are our foreign columns on $rel.
 -  my @otherrels = $othertable->relationships();
 -  my $otherrelationship;
 -  foreach my $otherrel (@otherrels) {
 -    # this may be a partial schema with the related source not being
 -    # available at all
 -    my $back = try { $othertable->related_source($otherrel) } or next;
 -
 -    # did we get back to ourselves?
 -    next unless $back->source_name eq $self->source_name;
 -
 -    my $otherrel_info = $othertable->relationship_info($otherrel);
 -    my @othertestconds;
 -
 -    if (ref $otherrel_info->{cond} eq 'HASH') {
 -      @othertestconds = ($otherrel_info->{cond});
 -    }
 -    elsif (ref $otherrel_info->{cond} eq 'ARRAY') {
 -      @othertestconds = @{$otherrel_info->{cond}};
 +  # columns are our foreign columns on $rel
 +  foreach my $other_rel ($other_rsrc->relationships) {
 +
 +    # only consider stuff that points back to us
 +    # "us" here is tricky - if we are in a schema registration, we want
 +    # to use the source_names, otherwise we will use the actual classes
 +
 +    # the schema may be partial
 +    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;
      }
      else {
 -      next;
 +      next unless $self->result_class eq $roundtrip_rsrc->result_class;
      }
  
 -    foreach my $othercond (@othertestconds) {
 -      my @other_cond = keys(%$othercond);
 -      my @other_refkeys = map {/^\w+\.(\w+)$/} @other_cond;
 -      my @other_keys = map {$othercond->{$_} =~ /^\w+\.(\w+)$/} @other_cond;
 -      next if (!$self->_compare_relationship_keys(\@refkeys, \@other_keys) ||
 -               !$self->_compare_relationship_keys(\@other_refkeys, \@keys));
 -      $ret->{$otherrel} =  $otherrel_info;
 -    }
 +    my $other_rel_info = $other_rsrc->relationship_info($other_rel);
 +
 +    # 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});
 +
 +    $ret->{$other_rel} = $other_rel_info if (
 +      $self->_compare_relationship_keys (
 +        [ keys %$stripped_cond ], [ values %$other_stripped_cond ]
 +      )
 +        and
 +      $self->_compare_relationship_keys (
 +        [ values %$stripped_cond ], [ keys %$other_stripped_cond ]
 +      )
 +    );
    }
 +
    return $ret;
  }
  
 +# all this does is removes the foreign/self prefix from a condition
 +sub __strip_relcond {
 +  +{
 +    map
 +      { map { /^ (?:foreign|self) \. (\w+) $/x } ($_, $_[1]{$_}) }
 +      keys %{$_[1]}
 +  }
 +}
 +
  sub compare_relationship_keys {
    carp 'compare_relationship_keys is a private method, stop calling it';
    my $self = shift;
  
  # Returns true if both sets of keynames are the same, false otherwise.
  sub _compare_relationship_keys {
 -  my ($self, $keys1, $keys2) = @_;
 -
 -  # Make sure every keys1 is in keys2
 -  my $found;
 -  foreach my $key (@$keys1) {
 -    $found = 0;
 -    foreach my $prim (@$keys2) {
 -      if ($prim eq $key) {
 -        $found = 1;
 -        last;
 -      }
 -    }
 -    last unless $found;
 -  }
 -
 -  # Make sure every key2 is in key1
 -  if ($found) {
 -    foreach my $prim (@$keys2) {
 -      $found = 0;
 -      foreach my $key (@$keys1) {
 -        if ($prim eq $key) {
 -          $found = 1;
 -          last;
 -        }
 -      }
 -      last unless $found;
 -    }
 -  }
 -
 -  return $found;
 +#  my ($self, $keys1, $keys2) = @_;
 +  return
 +    join ("\x00", sort @{$_[1]})
 +      eq
 +    join ("\x00", sort @{$_[2]})
 +  ;
  }
  
  # Returns the {from} structure used to express JOIN conditions
@@@ -1486,7 -1493,8 +1486,8 @@@ sub _resolve_join 
                 -alias => $as,
                 -relation_chain_depth => $seen->{-relation_chain_depth} || 0,
               },
-              $self->_resolve_condition($rel_info->{cond}, $as, $alias) ];
+              $self->_resolve_condition($rel_info->{cond}, $as, $alias, $join)
+           ];
    }
  }
  
@@@ -1538,14 -1546,89 +1539,89 @@@ sub resolve_condition 
    $self->_resolve_condition (@_);
  }
  
- # Resolves the passed condition to a concrete query fragment. If given an alias,
- # returns a join condition; if given an object, inverts that object to produce
- # a related conditional from that object.
- our $UNRESOLVABLE_CONDITION = \'1 = 0';
+ our $UNRESOLVABLE_CONDITION = \ '1 = 0';
  
+ # 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
+ # of a joinfree condition hash
  sub _resolve_condition {
-   my ($self, $cond, $as, $for) = @_;
-   if (ref $cond eq 'HASH') {
+   my ($self, $cond, $as, $for, $relname) = @_;
+   my $obj_rel = !!blessed $for;
+   if (ref $cond eq 'CODE') {
+     my $relalias = $obj_rel ? 'me' : $as;
+     my ($crosstable_cond, $joinfree_cond) = $cond->({
+       self_alias => $obj_rel ? $as : $for,
+       foreign_alias => $relalias,
+       self_resultsource => $self,
+       foreign_relname => $relname || ($obj_rel ? $as : $for),
+       self_rowobj => $obj_rel ? $for : undef
+     });
+     my $cond_cols;
+     if ($joinfree_cond) {
+       # FIXME sanity check until things stabilize, remove at some point
+       $self->throw_exception (
+         "A join-free condition returned for relationship '$relname' whithout a row-object to chain from"
+       ) unless $obj_rel;
+       # FIXME another sanity check
+       if (
+         ref $joinfree_cond ne 'HASH'
+           or
+         first { $_ !~ /^\Q$relalias.\E.+/ } keys %$joinfree_cond
+       ) {
+         $self->throw_exception (
+           "The join-free condition returned for relationship '$relname' must be a hash "
+          .'reference with all keys being valid columns on the related result source'
+         );
+       }
+       # normalize
+       for (values %$joinfree_cond) {
+         $_ = $_->{'='} if (
+           ref $_ eq 'HASH'
+             and
+           keys %$_ == 1
+             and
+           exists $_->{'='}
+         );
+       }
+       # see which parts of the joinfree cond are conditionals
+       my $relcol_list = { map { $_ => 1 } $self->related_source($relname)->columns };
+       for my $c (keys %$joinfree_cond) {
+         my ($colname) = $c =~ /^ (?: \Q$relalias.\E )? (.+)/x;
+         unless ($relcol_list->{$colname}) {
+           push @$cond_cols, $colname;
+           next;
+         }
+         if (
+           ref $joinfree_cond->{$c}
+             and
+           ref $joinfree_cond->{$c} ne 'SCALAR'
+             and
+           ref $joinfree_cond->{$c} ne 'REF'
+         ) {
+           push @$cond_cols, $colname;
+           next;
+         }
+       }
+       return wantarray ? ($joinfree_cond, 0, $cond_cols) : $joinfree_cond;
+     }
+     else {
+       return wantarray ? ($crosstable_cond, 1) : $crosstable_cond;
+     }
+   }
+   elsif (ref $cond eq 'HASH') {
      my %ret;
      foreach my $k (keys %{$cond}) {
        my $v = $cond->{$k};
        } elsif (!defined $as) { # undef, i.e. "no reverse object"
          $ret{$v} = undef;
        } else {
-         $ret{"${as}.${k}"} = "${for}.${v}";
+         $ret{"${as}.${k}"} = { -ident => "${for}.${v}" };
        }
      }
-     return \%ret;
-   } elsif (ref $cond eq 'ARRAY') {
-     return [ map { $self->_resolve_condition($_, $as, $for) } @$cond ];
-   } else {
-    die("Can't handle condition $cond yet :(");
+     return wantarray
+       ? ( \%ret, ($obj_rel || !defined $as || ref $as) ? 0 : 1 )
+       : \%ret
+     ;
+   }
+   elsif (ref $cond eq 'ARRAY') {
+     my (@ret, $crosstable);
+     for (@$cond) {
+       my ($cond, $crosstab) = $self->_resolve_condition($_, $as, $for, $relname);
+       push @ret, $cond;
+       $crosstable ||= $crosstab;
+     }
+     return wantarray ? (\@ret, $crosstable) : \@ret;
+   }
+   else {
+     $self->throw_exception ("Can't handle condition $cond for relationship '$relname' 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
@@@ -1646,6 -1740,7 +1733,7 @@@ sub _resolve_prefetch 
          "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 =~ /([^\.]+)$/);
            . 'Use at your own risk.'
          );
        }
        #my @col = map { (/^self\.(.+)$/ ? ("${as_prefix}.$1") : ()); }
        #              values %{$rel_info->{cond}};
        $collapse->{".${as_prefix}${pre}"} = [ $rel_source->_pri_cols ];
@@@ -1719,18 -1815,7 +1808,18 @@@ sub related_source 
    if( !$self->has_relationship( $rel ) ) {
      $self->throw_exception("No such relationship '$rel' on " . $self->source_name);
    }
 -  return $self->schema->source($self->relationship_info($rel)->{source});
 +
 +  # if we are not registered with a schema - just use the prototype
 +  # however if we do have a schema - ask for the source by name (and
 +  # throw in the process if all fails)
 +  if (my $schema = try { $self->schema }) {
 +    $schema->source($self->relationship_info($rel)->{source});
 +  }
 +  else {
 +    my $class = $self->relationship_info($rel)->{class};
 +    $self->ensure_class_loaded($class);
 +    $class->result_source_instance;
 +  }
  }
  
  =head2 related_class
@@@ -1787,10 -1872,7 +1876,10 @@@ sub handle 
  {
    my $global_phase_destroy;
  
 -  END { $global_phase_destroy++ }
 +  # SpeedyCGI runs END blocks every cycle but keeps object instances
 +  # hence we have to disable the globaldestroy hatch, and rely on the
 +  # eval trap below (which appears to work, but is risky done so late)
 +  END { $global_phase_destroy = 1 unless $CGI::SpeedyCGI::i_am_speedy }
  
    sub DESTROY {
      return if $global_phase_destroy;
      );
  
      # weaken our schema hold forcing the schema to find somewhere else to live
 -    weaken $_[0]->{schema};
 +    # during global destruction (if we have not yet bailed out) this will throw
 +    # which will serve as a signal to not try doing anything else
 +    local $@;
 +    eval {
 +      weaken $_[0]->{schema};
 +      1;
 +    } or do {
 +      $global_phase_destroy = 1;
 +      return;
 +    };
 +
  
 -    # if schema is still there reintroduce ourselves with strong refs back
 +    # if schema is still there reintroduce ourselves with strong refs back to us
      if ($_[0]->{schema}) {
        my $srcregs = $_[0]->{schema}->source_registrations;
        for (keys %$srcregs) {
 +        next unless $srcregs->{$_};
          $srcregs->{$_} = $_[0] if $srcregs->{$_} == $_[0];
        }
      }
    }
  }
  
 -sub STORABLE_freeze { nfreeze($_[0]->handle) }
 +sub STORABLE_freeze { Storable::nfreeze($_[0]->handle) }
  
  sub STORABLE_thaw {
    my ($self, $cloning, $ice) = @_;
 -  %$self = %{ (thaw $ice)->resolve };
 +  %$self = %{ (Storable::thaw($ice))->resolve };
  }
  
  =head2 throw_exception
diff --combined lib/DBIx/Class/Row.pm
@@@ -8,6 -8,7 +8,6 @@@ use base qw/DBIx::Class/
  use DBIx::Class::Exception;
  use Scalar::Util 'blessed';
  use Try::Tiny;
 -use namespace::clean;
  
  ###
  ### Internal method
@@@ -20,8 -21,6 +20,8 @@@ BEGIN 
        : sub () { 0 };
  }
  
 +use namespace::clean;
 +
  =head1 NAME
  
  DBIx::Class::Row - Basic row methods
@@@ -1063,7 -1062,7 +1063,7 @@@ sub copy 
      next unless $rel_info->{attrs}{cascade_copy};
  
      my $resolved = $self->result_source->_resolve_condition(
-       $rel_info->{cond}, $rel, $new
+       $rel_info->{cond}, $rel, $new, $rel
      );
  
      my $copied = $rels_copied->{ $rel_info->{source} } ||= {};
@@@ -1,5 -1,8 +1,8 @@@
  package DBIx::Class::SQLMaker;
  
+ use strict;
+ use warnings;
  =head1 NAME
  
  DBIx::Class::SQLMaker - An SQL::Abstract-based SQL maker class
@@@ -35,14 -38,12 +38,13 @@@ Currently the enhancements to L<SQL::Ab
  use base qw/
    DBIx::Class::SQLMaker::LimitDialects
    SQL::Abstract
 -  Class::Accessor::Grouped
 +  DBIx::Class
  /;
  use mro 'c3';
- use strict;
- use warnings;
  use Sub::Name 'subname';
 -use Carp::Clan qw/^DBIx::Class|^SQL::Abstract|^Try::Tiny/;
 +use DBIx::Class::Carp;
 +use DBIx::Class::Exception;
  use namespace::clean;
  
  __PACKAGE__->mk_group_accessors (simple => qw/quote_char name_sep limit_dialect/);
@@@ -55,27 -56,25 +57,27 @@@ sub _quote_chars 
    ;
  }
  
 +# FIXME when we bring in the storage weaklink, check its schema
 +# weaklink and channel through $schema->throw_exception
 +sub throw_exception { DBIx::Class::Exception->throw($_[1]) }
 +
  BEGIN {
 -  # reinstall the carp()/croak() functions imported into SQL::Abstract
 -  # as Carp and Carp::Clan do not like each other much
 +  # reinstall the belch()/puke() functions of SQL::Abstract with custom versions
 +  # that use DBIx::Class::Carp/DBIx::Class::Exception instead of plain Carp
    no warnings qw/redefine/;
 -  no strict qw/refs/;
 -  for my $f (qw/carp croak/) {
 -
 -    my $orig = \&{"SQL::Abstract::$f"};
 -    my $clan_import = \&{$f};
 -    *{"SQL::Abstract::$f"} = subname "SQL::Abstract::$f" =>
 -      sub {
 -        if (Carp::longmess() =~ /DBIx::Class::SQLMaker::[\w]+ .+? called \s at/x) {
 -          goto $clan_import;
 -        }
 -        else {
 -          goto $orig;
 -        }
 -      };
 -  }
 +
 +  *SQL::Abstract::belch = subname 'SQL::Abstract::belch' => sub (@) {
 +    my($func) = (caller(1))[3];
 +    carp "[$func] Warning: ", @_;
 +  };
 +
 +  *SQL::Abstract::puke = subname 'SQL::Abstract::puke' => sub (@) {
 +    my($func) = (caller(1))[3];
 +    __PACKAGE__->throw_exception("[$func] Fatal: " . join ('',  @_));
 +  };
 +
 +  # Current SQLA pollutes its namespace - clean for the time being
 +  namespace::clean->clean_subroutines(qw/SQL::Abstract carp croak confess/);
  }
  
  # the "oh noes offset/top without limit" constant
@@@ -105,7 -104,7 +107,7 @@@ sub _where_op_IDENT 
    my $self = shift;
    my ($op, $rhs) = splice @_, -2;
    if (ref $rhs) {
 -    croak "-$op takes a single scalar argument (a quotable identifier)";
 +    $self->throw_exception("-$op takes a single scalar argument (a quotable identifier)");
    }
  
    # in case we are called as a top level special op (no '=')
@@@ -127,7 -126,7 +129,7 @@@ sub _where_op_VALUE 
    my $lhs = shift;
  
    my @bind = [
 -    ($lhs || $self->{_nested_func_lhs} || croak "Unable to find bindtype for -value $rhs"),
 +    ($lhs || $self->{_nested_func_lhs} || $self->throw_exception("Unable to find bindtype for -value $rhs") ),
      $rhs
    ];
  
    ;
  }
  
 -my $callsites_warned;
  sub _where_op_NEST {
 -  # determine callsite obeying Carp::Clan rules (fucking ugly but don't have better ideas)
 -  my $callsite = do {
 -    my $w;
 -    local $SIG{__WARN__} = sub { $w = shift };
 -    carp;
 -    $w
 -  };
 -
 -  carp ("-nest in search conditions is deprecated, you most probably wanted:\n"
 +  carp_unique ("-nest in search conditions is deprecated, you most probably wanted:\n"
        .q|{..., -and => [ \%cond0, \@cond1, \'cond2', \[ 'cond3', [ col => bind ] ], etc. ], ... }|
 -  ) unless $callsites_warned->{$callsite}++;
 +  );
  
    shift->next::method(@_);
  }
@@@ -159,13 -167,13 +161,13 @@@ sub select 
    $fields = $self->_recurse_fields($fields);
  
    if (defined $offset) {
 -    croak ('A supplied offset must be a non-negative integer')
 +    $self->throw_exception('A supplied offset must be a non-negative integer')
        if ( $offset =~ /\D/ or $offset < 0 );
    }
    $offset ||= 0;
  
    if (defined $limit) {
 -    croak ('A supplied limit must be a positive integer')
 +    $self->throw_exception('A supplied limit must be a positive integer')
        if ( $limit =~ /\D/ or $limit <= 0 );
    }
    elsif ($offset) {
          ||
        do {
          my $dialect = $self->limit_dialect
 -          or croak "Unable to generate SQL-limit - no limit dialect specified on $self, and no emulate_limit method found";
 +          or $self->throw_exception( "Unable to generate SQL-limit - no limit dialect specified on $self, and no emulate_limit method found" );
          $self->can ("_$dialect")
 -          or croak (__PACKAGE__ . " does not implement the requested dialect '$dialect'");
 +          or $self->throw_exception(__PACKAGE__ . " does not implement the requested dialect '$dialect'");
        }
      ;
  
@@@ -218,7 -226,7 +220,7 @@@ my $for_syntax = 
  };
  sub _lock_select {
    my ($self, $type) = @_;
 -  my $sql = $for_syntax->{$type} || croak "Unknown SELECT .. FOR type '$type' requested";
 +  my $sql = $for_syntax->{$type} || $self->throw_exception( "Unknown SELECT .. FOR type '$type' requested" );
    return " $sql";
  }
  
@@@ -266,11 -274,11 +268,11 @@@ sub _recurse_fields 
  
      # there should be only one pair
      if (@toomany) {
 -      croak "Malformed select argument - too many keys in hash: " . join (',', keys %$fields );
 +      $self->throw_exception( "Malformed select argument - too many keys in hash: " . join (',', keys %$fields ) );
      }
  
      if (lc ($func) eq 'distinct' && ref $args eq 'ARRAY' && @$args > 1) {
 -      croak (
 +      $self->throw_exception (
          'The select => { distinct => ... } syntax is not supported for multiple columns.'
         .' Instead please use { group_by => [ qw/' . (join ' ', @$args) . '/ ] }'
         .' or { select => [ qw/' . (join ' ', @$args) . '/ ], distinct => 1 }'
      return $$fields->[0];
    }
    else {
 -    croak($ref . qq{ unexpected in _recurse_fields()})
 +    $self->throw_exception( $ref . qq{ unexpected in _recurse_fields()} );
    }
  }
  
@@@ -357,38 -365,25 +359,38 @@@ sub _table 
      elsif ($ref eq 'HASH') {
        return $_[0]->_recurse_from($_[1]);
      }
 +    elsif ($ref eq 'REF' && ref ${$_[1]} eq 'ARRAY') {
 +      my ($sql, @bind) = @{ ${$_[1]} };
 +      push @{$_[0]->{from_bind}}, @bind;
 +      return $sql
 +    }
    }
 -
    return $_[0]->next::method ($_[1]);
  }
  
  sub _generate_join_clause {
      my ($self, $join_type) = @_;
  
 +    $join_type = $self->{_default_jointype}
 +      if ! defined $join_type;
 +
      return sprintf ('%s JOIN ',
 -      $join_type ?  ' ' . $self->_sqlcase($join_type) : ''
 +      $join_type ?  $self->_sqlcase($join_type) : ''
      );
  }
  
  sub _recurse_from {
 -  my ($self, $from, @join) = @_;
 -  my @sqlf;
 -  push @sqlf, $self->_from_chunk_to_sql($from);
 +  my $self = shift;
 +
 +  return join (' ', $self->_gen_from_blocks(@_) );
 +}
  
 -  for (@join) {
 +sub _gen_from_blocks {
 +  my ($self, $from, @joins) = @_;
 +
 +  my @fchunks = $self->_from_chunk_to_sql($from);
 +
 +  for (@joins) {
      my ($to, $on) = @$_;
  
      # check whether a join type exists
        $join_type =~ s/^\s+ | \s+$//xg;
      }
  
 -    $join_type = $self->{_default_jointype} if not defined $join_type;
 -
 -    push @sqlf, $self->_generate_join_clause( $join_type );
 +    my @j = $self->_generate_join_clause( $join_type );
  
      if (ref $to eq 'ARRAY') {
 -      push(@sqlf, '(', $self->_recurse_from(@$to), ')');
 -    } else {
 -      push(@sqlf, $self->_from_chunk_to_sql($to));
 +      push(@j, '(', $self->_recurse_from(@$to), ')');
 +    }
 +    else {
 +      push(@j, $self->_from_chunk_to_sql($to));
      }
  
-     push(@j, ' ON ', $self->_join_condition($on));
+     my ($sql, @bind) = $self->_join_condition($on);
 -    push(@sqlf, ' ON ', $sql);
++    push(@j, ' ON ', $sql);
+     push @{$self->{from_bind}}, @bind;
 +
 +    push @fchunks, join '', @j;
    }
  
 -  return join('', @sqlf);
 +  return @fchunks;
  }
  
  sub _from_chunk_to_sql {
          ( grep { $_ !~ /^\-/ } keys %$fromspec )
        );
  
 -      croak "Only one table/as pair expected in from-spec but an exra '$toomuch' key present"
 +      $self->throw_exception( "Only one table/as pair expected in from-spec but an exra '$toomuch' key present" )
          if defined $toomuch;
  
        ($self->_from_chunk_to_sql($table), $self->_quote($as) );
  sub _join_condition {
    my ($self, $cond) = @_;
  
-   if (ref $cond eq 'HASH') {
-     my %j;
-     for (keys %$cond) {
-       my $v = $cond->{$_};
-       if (ref $v) {
-         $self->throw_exception (ref($v) . qq{ reference arguments are not supported in JOINS - try using \"..." instead'})
-             if ref($v) ne 'SCALAR';
-         $j{$_} = $v;
-       }
-       else {
-         my $x = '= '.$self->_quote($v); $j{$_} = \$x;
-       }
-     };
-     return scalar($self->_recurse_where(\%j));
-   } elsif (ref $cond eq 'ARRAY') {
-     return join(' OR ', map { $self->_join_condition($_) } @$cond);
-   } else {
-     die "Can't handle this yet!";
+   # Backcompat for the old days when a plain hashref
+   # { 't1.col1' => 't2.col2' } meant ON t1.col1 = t2.col2
+   # Once things settle we should start warning here so that
+   # folks unroll their hacks
+   if (
+     ref $cond eq 'HASH'
+       and
+     keys %$cond == 1
+       and
+     (keys %$cond)[0] =~ /\./
+       and
+     ! ref ( (values %$cond)[0] )
+   ) {
+     $cond = { keys %$cond => { -ident => values %$cond } }
    }
+   elsif ( ref $cond eq 'ARRAY' ) {
+     # do our own ORing so that the hashref-shim above is invoked
+     my @parts;
+     my @binds;
+     foreach my $c (@$cond) {
+       my ($sql, @bind) = $self->_join_condition($c);
+       push @binds, @bind;
+       push @parts, $sql;
+     }
+     return join(' OR ', @parts), @binds;
+   }
+   return $self->_recurse_where($cond);
  }
  
  1;
@@@ -1,20 -1,17 +1,20 @@@
  package # Hide from PAUSE
    DBIx::Class::SQLMaker::OracleJoins;
  
 +use warnings;
 +use strict;
 +
  use base qw( DBIx::Class::SQLMaker::Oracle );
 -use Carp::Clan qw/^DBIx::Class|^SQL::Abstract/;
  
  sub select {
    my ($self, $table, $fields, $where, $rs_attrs, @rest) = @_;
  
 +  # pull out all join conds as regular WHEREs from all extra tables
    if (ref($table) eq 'ARRAY') {
 -    $where = $self->_oracle_joins($where, @{ $table });
 +    $where = $self->_oracle_joins($where, @{ $table }[ 1 .. $#$table ]);
    }
  
 -  return $self->SUPER::select($table, $fields, $where, $rs_attrs, @rest);
 +  return $self->next::method($table, $fields, $where, $rs_attrs, @rest);
  }
  
  sub _recurse_from {
@@@ -37,9 -34,9 +37,9 @@@
  }
  
  sub _oracle_joins {
 -  my ($self, $where, $from, @join) = @_;
 -  my $join_where = {};
 -  $self->_recurse_oracle_joins($join_where, $from, @join);
 +  my ($self, $where, @join) = @_;
 +  my $join_where = $self->_recurse_oracle_joins(@join);
 +
    if (keys %$join_where) {
      if (!defined($where)) {
        $where = $join_where;
  }
  
  sub _recurse_oracle_joins {
 -  my ($self, $where, $from, @join) = @_;
 +  my $self = shift;
  
 -  foreach my $j (@join) {
 +  my @where;
 +  for my $j (@_) {
      my ($to, $on) = @{ $j };
  
 -    if (ref $to eq 'ARRAY') {
 -      $self->_recurse_oracle_joins($where, @{ $to });
 -    }
 +    push @where, $self->_recurse_oracle_joins(@{ $to })
 +      if (ref $to eq 'ARRAY');
  
 -    my $to_jt      = ref $to eq 'ARRAY' ? $to->[0] : $to;
 +    my $join_opts  = ref $to eq 'ARRAY' ? $to->[0] : $to;
      my $left_join  = q{};
      my $right_join = q{};
  
 -    if (ref $to_jt eq 'HASH' and exists $to_jt->{-join_type}) {
 +    if (ref $join_opts eq 'HASH' and my $jt = $join_opts->{-join_type}) {
        #TODO: Support full outer joins -- this would happen much earlier in
        #the sequence since oracle 8's full outer join syntax is best
        #described as INSANE.
 -      croak "Can't handle full outer joins in Oracle 8 yet!\n"
 -        if $to_jt->{-join_type} =~ /full/i;
 +      $self->throw_exception("Can't handle full outer joins in Oracle 8 yet!\n")
 +        if $jt =~ /full/i;
  
 -      $left_join  = q{(+)} if $to_jt->{-join_type} =~ /left/i
 -        && $to_jt->{-join_type} !~ /inner/i;
 +      $left_join  = q{(+)} if $jt =~ /left/i
 +        && $jt !~ /inner/i;
  
 -      $right_join = q{(+)} if $to_jt->{-join_type} =~ /right/i
 -        && $to_jt->{-join_type} !~ /inner/i;
 +      $right_join = q{(+)} if $jt =~ /right/i
 +        && $jt !~ /inner/i;
      }
  
 -    foreach my $lhs (keys %{ $on }) {
 -      $where->{$lhs . $left_join} = \"= $on->{ $lhs }$right_join";
 -    }
++    # sadly SQLA treats where($scalar) as literal, so we need to jump some hoops
 +    push @where, map { \sprintf ('%s%s = %s%s',
-       $self->_quote($_),
++      ref $_ ? $self->_recurse_where($_) : $self->_quote($_),
 +      $left_join,
-       $self->_quote($on->{$_}),
++      ref $on->{$_} ? $self->_recurse_where($on->{$_}) : $self->_quote($on->{$_}),
 +      $right_join,
 +    )} keys %$on;
    }
 +
 +  return { -and => \@where };
  }
  
  1;
@@@ -102,8 -94,9 +103,8 @@@ DBIx::Class::SQLMaker::OracleJoins - Pr
  
  =head1 PURPOSE
  
 -This module was originally written to support Oracle < 9i where ANSI joins
 -weren't supported at all, but became the module for Oracle >= 8 because
 -Oracle's optimising of ANSI joins is horrible.
 +This module is used with Oracle < 9.0 due to lack of support for standard
 +ANSI join syntax.
  
  =head1 SYNOPSIS
  
@@@ -129,16 -122,25 +130,16 @@@ it's already too late
  
  =over
  
 -=item select ($\@$;$$@)
 -
 -Replaces DBIx::Class::SQLMaker's select() method, which calls _oracle_joins()
 -to modify the column and table list before calling SUPER::select().
 -
 -=item _recurse_from ($$\@)
 -
 -Recursive subroutine that builds the table list.
 -
 -=item _oracle_joins ($$$@)
 +=item select
  
 -Creates the left/right relationship in the where query.
 +Overrides DBIx::Class::SQLMaker's select() method, which calls _oracle_joins()
 +to modify the column and table list before calling next::method().
  
  =back
  
  =head1 BUGS
  
 -Does not support full outer joins.
 -Probably lots more.
 +Does not support full outer joins (however neither really does DBIC itself)
  
  =head1 SEE ALSO
  
diff --combined t/relationship/custom.t
index 0000000,e60bad6..99a0786
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,214 +1,231 @@@
+ use strict;
+ use warnings;
+ use Test::More;
+ use Test::Exception;
+ use lib qw(t/lib);
+ use DBICTest;
+ use DBIC::SqlMakerTest;
+ my $schema = DBICTest->init_schema();
+ $schema->resultset('Artist')->delete;
+ $schema->resultset('CD')->delete;
+ my $artist  = $schema->resultset("Artist")->create({ artistid => 21, name => 'Michael Jackson', rank => 20 });
+ my $artist2 = $schema->resultset("Artist")->create({ artistid => 22, name => 'Chico Buarque', rank => 1 }) ;
+ my $artist3 = $schema->resultset("Artist")->create({ artistid => 23, name => 'Ziraldo', rank => 1 });
+ my $artist4 = $schema->resultset("Artist")->create({ artistid => 24, name => 'Paulo Caruso', rank => 20 });
+ my @artworks;
+ foreach my $year (1975..1985) {
+   my $cd = $artist->create_related('cds', { year => $year, title => 'Compilation from ' . $year });
+   push @artworks, $cd->create_related('artwork', {});
+ }
+ foreach my $year (1975..1995) {
+   my $cd = $artist2->create_related('cds', { year => $year, title => 'Compilation from ' . $year });
+   push @artworks, $cd->create_related('artwork', {});
+ }
+ foreach my $artwork (@artworks) {
+   $artwork->create_related('artwork_to_artist', { artist => $_ }) for ($artist3, $artist4);
+ }
+ my $cds_80s_rs = $artist->cds_80s;
+ is_same_sql_bind(
+   $cds_80s_rs->as_query,
+   '(
+     SELECT me.cdid, me.artist, me.title, me.year, me.genreid, me.single_track
+       FROM cd me
+     WHERE ( ( me.artist = ? AND ( me.year < ? AND me.year > ? ) ) )
+   )',
+   [
 -    [ 'me.artist' => 21   ],
 -    [ 'me.year' => 1990 ],
 -    [ 'me.year' => 1979 ],
 -  ]
++    [
++      { sqlt_datatype => 'integer', dbic_colname => 'me.artist' }
++        => 21
++    ],
++    [
++      { sqlt_datatype => 'varchar', sqlt_size => 100, dbic_colname => 'me.year' }
++        => 1990
++    ],
++    [
++      { sqlt_datatype => 'varchar', sqlt_size => 100, dbic_colname => 'me.year' }
++        => 1979
++    ],
++  ],
+ );
+ my @cds_80s = $cds_80s_rs->all;
+ is(@cds_80s, 6, '6 80s cds found (1980 - 1985)');
+ map { ok($_->year < 1990 && $_->year > 1979) } @cds_80s;
+ my $cds_90s_rs = $artist2->cds_90s;
+ is_same_sql_bind(
+   $cds_90s_rs->as_query,
+   '(
+     SELECT me.cdid, me.artist, me.title, me.year, me.genreid, me.single_track
+       FROM artist artist__row
+       JOIN cd me
+         ON ( me.artist = artist__row.artistid AND ( me.year < ? AND me.year > ? ) )
+       WHERE ( artist__row.artistid = ? )
+   )',
+   [
 -    [ 'me.year' => 2000 ],
 -    [ 'me.year' => 1989 ],
 -    [ 'artist__row.artistid' => 22 ],
++    [
++      { sqlt_datatype => 'varchar', sqlt_size => 100, dbic_colname => 'me.year' }
++        => 2000
++    ],
++    [
++      { sqlt_datatype => 'varchar', sqlt_size => 100, dbic_colname => 'me.year' }
++        => 1989
++    ],
++    [ { sqlt_datatype => 'integer', dbic_colname => 'artist__row.artistid' }
++        => 22
++    ],
+   ]
+ );
+ my @cds_90s = $cds_90s_rs->all;
+ is(@cds_90s, 6, '6 90s cds found (1990 - 1995) even with non-optimized search');
+ map { ok($_->year < 2000 && $_->year > 1989) } @cds_90s;
+ lives_ok {
+   my @cds_90s_95 = $artist2->cds_90s->search({ 'me.year' => 1995 });
+   is(@cds_90s_95, 1, '1 90s (95) cds found even with non-optimized search');
+   map { ok($_->year == 1995) } @cds_90s_95;
+ } 'should preserve chain-head "me" alias (API-consistency)';
+ # search for all artists prefetching published cds in the 80s...
+ my @all_artists_with_80_cds = $schema->resultset("Artist")->search
+   ({ 'cds_80s.cdid' => { '!=' => undef } }, { join => 'cds_80s', distinct => 1 });
+ is_deeply(
+   [ sort ( map { $_->year } map { $_->cds_80s->all } @all_artists_with_80_cds ) ],
+   [ sort (1980..1989, 1980..1985) ],
+   '16 correct cds found'
+ );
+ TODO: {
+ local $TODO = 'Prefetch on custom rels can not work until the collapse rewrite is finished '
+   . '(currently collapser requires a right-side (which is indeterministic) order-by)';
+ lives_ok {
+ my @all_artists_with_80_cds_pref = $schema->resultset("Artist")->search
+   ({ 'cds_80s.cdid' => { '!=' => undef } }, { prefetch => 'cds_80s' });
+ is_deeply(
+   [ sort ( map { $_->year } map { $_->cds_80s->all } @all_artists_with_80_cds_pref ) ],
+   [ sort (1980..1989, 1980..1985) ],
+   '16 correct cds found'
+ );
+ } 'prefetchy-fetchy-fetch';
+ } # end of TODO
+ # 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'/,
+ 'Create failed - complex cond';
+ # now supply an explicit arg overwriting the ambiguous cond
+ my $id_2020 = $artist->create_related('cds_80s', { title => 'related creation 2', year => '2020' })->id;
+ is(
+   $schema->resultset('CD')->find($id_2020)->title,
+   'related creation 2',
+   '2020 CD created correctly'
+ );
+ # try a default year from a specific rel
+ my $id_1984 = $artist->create_related('cds_84', { title => 'related creation 3' })->id;
+ is(
+   $schema->resultset('CD')->find($id_1984)->title,
+   'related creation 3',
+   '1984 CD created correctly'
+ );
+ # try a specific everything via a non-simplified rel
+ throws_ok {
+   $artist->create_related('cds_90s', { title => 'related_creation 4', year => '2038' });
+ } qr/\QCustom relationship 'cds_90s' does not resolve to a join-free condition fragment/,
+ 'Create failed - non-simplified rel';
+ # Do a self-join last-entry search
+ my @last_tracks;
+ for my $cd ($schema->resultset('CD')->search ({}, { order_by => 'cdid'})->all) {
+   push @last_tracks, $cd->tracks
+                          ->search ({}, { order_by => { -desc => 'position'} })
+                           ->next || ();
+ }
+ my $last_tracks_rs = $schema->resultset('Track')->search (
+   {'next_track.trackid' => undef},
+   { join => 'next_track', order_by => 'me.cd' },
+ );
+ is_deeply (
+   [$last_tracks_rs->get_column ('trackid')->all],
+   [ map { $_->trackid } @last_tracks ],
+   'last group-entry via self-join works',
+ );
+ my $artwork = $schema->resultset('Artwork')->search({},{ order_by => 'cd_id' })->first;
+ my @artists = $artwork->artists->all;
+ is(scalar @artists, 2, 'the two artists are associated');
+ my @artwork_artists = $artwork->artwork_to_artist->all;
+ foreach (@artwork_artists) {
+   lives_ok {
+     my $artista = $_->artist;
+     my $artistb = $_->artist_test_m2m;
+     ok($artista->rank < 10 ? $artistb : 1, 'belongs_to with custom rel works.');
+     my $artistc = $_->artist_test_m2m_noopt;
+     ok($artista->rank < 10 ? $artistc : 1, 'belongs_to with custom rel works even in non-simplified.');
+   } 'belongs_to works with custom rels';
+ }
+ @artists = ();
+ lives_ok {
+   @artists = $artwork->artists_test_m2m2->all;
+ } 'manytomany with extended rels in the has many works';
+ is(scalar @artists, 2, 'two artists');
+ @artists = ();
+ lives_ok {
+   @artists = $artwork->artists_test_m2m->all;
+ } 'can fetch many to many with optimized version';
+ is(scalar @artists, 1, 'only one artist is associated');
+ @artists = ();
+ lives_ok {
+   @artists = $artwork->artists_test_m2m_noopt->all;
+ } 'can fetch many to many with non-optimized version';
+ is(scalar @artists, 1, 'only one artist is associated');
+ # Make a single for each last_track
+ my @singles = map {
+   $_->create_related('cd_single', {
+     title => $_->title . ' (the single)',
+     artist => $artist,
+     year => 1999,
+   }) } @last_tracks
+ ;
+ # See if chaining works
+ is_deeply (
+   [ map { $_->title } $last_tracks_rs->search_related('cd_single')->all ],
+   [ map { $_->title } @singles ],
+   'Retrieved singles in proper order'
+ );
+ # See if prefetch works
+ is_deeply (
+   [ map { $_->cd_single->title } $last_tracks_rs->search({}, { prefetch => 'cd_single' })->all ],
+   [ map { $_->title } @singles ],
+   'Prefetched singles in proper order'
+ );
+ done_testing;