Merge 'trunk' into 'on_connect_call'
Rafael Kitover [Mon, 22 Jun 2009 23:05:13 +0000 (23:05 +0000)]
r5588@hlagh (orig r6733):  ribasushi | 2009-06-20 01:16:02 -0700
todoify skip
r5590@hlagh (orig r6735):  ribasushi | 2009-06-20 03:37:52 -0700
Clarify test
r5594@hlagh (orig r6739):  ribasushi | 2009-06-20 06:22:06 -0700
Disambiguate populate() return
r5603@hlagh (orig r6742):  ribasushi | 2009-06-20 14:30:23 -0700
 r6737@Thesaurus (orig r6736):  ribasushi | 2009-06-20 12:39:34 +0200
 new branch to streamline count() and introduce count_rs()
 r6738@Thesaurus (orig r6737):  ribasushi | 2009-06-20 12:44:09 +0200
 Add count_rs, move the code back from DBI - leave only sql specific hooks
 r6739@Thesaurus (orig r6738):  ribasushi | 2009-06-20 12:54:11 +0200
 Test count_rs
 r6742@Thesaurus (orig r6741):  ribasushi | 2009-06-20 23:30:10 +0200
 More tests and a really working count_rs

r5613@hlagh (orig r6752):  ribasushi | 2009-06-21 00:00:21 -0700
Clenaup text
r5614@hlagh (orig r6753):  ribasushi | 2009-06-21 05:37:56 -0700
make_column_dirty fix
r5617@hlagh (orig r6755):  ribasushi | 2009-06-21 14:12:40 -0700
Fix borked test

1  2 
lib/DBIx/Class/Storage/DBI.pm

@@@ -14,15 -14,13 +14,15 @@@ use List::Util()
  
  __PACKAGE__->mk_group_accessors('simple' =>
      qw/_connect_info _dbi_connect_info _dbh _sql_maker _sql_maker_opts
 -       _conn_pid _conn_tid transaction_depth _dbh_autocommit savepoints/
 +       _conn_pid _conn_tid transaction_depth _dbh_autocommit _on_connect_do
 +       _on_disconnect_do _on_connect_do_store _on_disconnect_do_store
 +       savepoints/
  );
  
  # the values for these accessors are picked out (and deleted) from
  # the attribute hashref passed to connect_info
  my @storage_options = qw/
 -  on_connect_do on_disconnect_do disable_sth_caching unsafe auto_savepoint
 +  on_connect_call on_disconnect_call disable_sth_caching unsafe auto_savepoint
  /;
  __PACKAGE__->mk_group_accessors('simple' => @storage_options);
  
@@@ -179,91 -177,6 +179,91 @@@ immediately before disconnecting from t
  Note, this only runs if you explicitly call L</disconnect> on the
  storage object.
  
 +=item on_connect_call
 +
 +A more generalized form of L</on_connect_do> that calls the specified
 +C<connect_call_METHOD> methods in your storage driver.
 +
 +  on_connect_do => 'select 1'
 +
 +is equivalent to:
 +
 +  on_connect_call => [ [ do_sql => 'select 1' ] ]
 +
 +Its values may contain:
 +
 +=over
 +
 +=item a scalar
 +
 +Will call the C<connect_call_METHOD> method.
 +
 +=item a code reference
 +
 +Will execute C<< $code->($storage) >>
 +
 +=item an array reference
 +
 +Each value can be a method name or code reference.
 +
 +=item an array of arrays
 +
 +For each array, the first item is taken to be the C<connect_call_> method name
 +or code reference, and the rest are parameters to it.
 +
 +=back
 +
 +Some predefined storage methods you may use:
 +
 +=over
 +
 +=item do_sql
 +
 +Executes a SQL string or a code reference that returns a SQL string. This is
 +what L</on_connect_do> and L</on_disconnect_do> use.
 +
 +It can take:
 +
 +=over
 +
 +=item a scalar
 +
 +Will execute the scalar as SQL.
 +
 +=item an arrayref
 +
 +Taken to be arguments to L<DBI/do>, the SQL string optionally followed by the
 +attributes hashref and bind values.
 +
 +=item a code reference
 +
 +Will execute C<< $code->($storage) >> and execute the return array refs as
 +above.
 +
 +=back
 +
 +=item datetime_setup
 +
 +Execute any statements necessary to initialize the database session to return
 +and accept datetime/timestamp values used with
 +L<DBIx::Class::InflateColumn::DateTime>.
 +
 +Only necessary for some databases, see your specific storage driver for
 +implementation details.
 +
 +=back
 +
 +=item on_disconnect_call
 +
 +Takes arguments in the same form as L</on_connect_call> and executes them
 +immediately before disconnecting from the database.
 +
 +Calls the C<disconnect_call_METHOD> methods as opposed to the
 +C<connect_call_METHOD> methods called by L</on_connect_call>.
 +
 +Note, this only runs if you explicitly call L</disconnect> on the
 +storage object.
 +
  =item disable_sth_caching
  
  If set to a true value, this option will disable the caching of
@@@ -434,11 -347,6 +434,11 @@@ sub connect_info 
          $self->_sql_maker_opts->{$sql_maker_opt} = $opt_val;
        }
      }
 +    for my $connect_do_opt (qw/on_connect_do on_disconnect_do/) {
 +      if(my $opt_val = delete $attrs{$connect_do_opt}) {
 +        $self->$connect_do_opt($opt_val);
 +      }
 +    }
    }
  
    %attrs = () if (ref $args[0] eq 'CODE');  # _connect() never looks past $args[0] in this case
  
  This method is deprecated in favour of setting via L</connect_info>.
  
 +=cut
 +
 +sub on_connect_do {
 +  my $self = shift;
 +  $self->_setup_connect_do(on_connect_do => @_);
 +}
 +
 +=head2 on_disconnect_do
 +
 +This method is deprecated in favour of setting via L</connect_info>.
 +
 +=cut
 +
 +sub on_disconnect_do {
 +  my $self = shift;
 +  $self->_setup_connect_do(on_disconnect_do => @_);
 +}
 +
 +sub _setup_connect_do {
 +  my ($self, $opt) = (shift, shift);
 +
 +  my $accessor = "_$opt";
 +  my $store    = "_${opt}_store";
 +
 +  return $self->$accessor if not @_;
 +
 +  my $val = shift;
 +
 +  if (not defined $val) {
 +    $self->$accessor(undef);
 +    $self->$store(undef);
 +    return;
 +  }
 +
 +  my @store;
 +
 +  if (not ref($val)) {
 +    push @store, [ 'do_sql', $val ];
 +  } elsif (ref($val) eq 'CODE') {
 +    push @store, $val;
 +  } elsif (ref($val) eq 'ARRAY') {
 +    push @store, map [ 'do_sql', $_ ], @$val;
 +  } else {
 +    $self->throw_exception("Invalid type for $opt ".ref($val));
 +  }
 +
 +  $self->$store(\@store);
 +  $self->$accessor($val);
 +}
  
  =head2 dbh_do
  
@@@ -647,12 -506,8 +647,12 @@@ sub disconnect 
    my ($self) = @_;
  
    if( $self->connected ) {
 -    my $connection_do = $self->on_disconnect_do;
 -    $self->_do_connection_actions($connection_do) if ref($connection_do);
 +    if (my $connection_call = $self->on_disconnect_call) {
 +      $self->_do_connection_actions(disconnect_call_ => $connection_call)
 +    }
 +    if (my $connection_do   = $self->_on_disconnect_do_store) {
 +      $self->_do_connection_actions(disconnect_call_ => $connection_do)
 +    }
  
      $self->_dbh->rollback unless $self->_dbh_autocommit;
      $self->_dbh->disconnect;
@@@ -769,12 -624,8 +769,12 @@@ sub _populate_dbh 
    #  there is no transaction in progress by definition
    $self->{transaction_depth} = $self->_dbh_autocommit ? 0 : 1;
  
 -  my $connection_do = $self->on_connect_do;
 -  $self->_do_connection_actions($connection_do) if $connection_do;
 +  if (my $connection_call = $self->on_connect_call) {
 +    $self->_do_connection_actions(connect_call_ => $connection_call)
 +  }
 +  if (my $connection_do = $self->_on_connect_do_store) {
 +    $self->_do_connection_actions(connect_call_ => $connection_do)
 +  }
  }
  
  sub _determine_driver {
  }
  
  sub _do_connection_actions {
 -  my $self = shift;
 -  my $connection_do = shift;
 -
 -  if (!ref $connection_do) {
 -    $self->_do_query($connection_do);
 -  }
 -  elsif (ref $connection_do eq 'ARRAY') {
 -    $self->_do_query($_) foreach @$connection_do;
 -  }
 -  elsif (ref $connection_do eq 'CODE') {
 -    $connection_do->($self);
 -  }
 -  else {
 -    $self->throw_exception (sprintf ("Don't know how to process conection actions of type '%s'", ref $connection_do) );
 +  my $self          = shift;
 +  my $method_prefix = shift;
 +  my $call          = shift;
 +
 +  if (not ref($call)) {
 +    my $method = $method_prefix . $call;
 +    $self->$method(@_);
 +  } elsif (ref($call) eq 'CODE') {
 +    $self->$call(@_);
 +  } elsif (ref($call) eq 'ARRAY') {
 +    if (ref($call->[0]) ne 'ARRAY') {
 +      $self->_do_connection_actions($method_prefix, $_) for @$call;
 +    } else {
 +      $self->_do_connection_actions($method_prefix, @$_) for @$call;
 +    }
 +  } else {
 +    $self->throw_exception (sprintf ("Don't know how to process conection actions of type '%s'", ref($call)) );
    }
  
    return $self;
  }
  
 +sub connect_call_do_sql {
 +  my $self = shift;
 +  $self->_do_query(@_);
 +}
 +
 +sub disconnect_call_do_sql {
 +  my $self = shift;
 +  $self->_do_query(@_);
 +}
 +
 +# override in db-specific backend when necessary
 +sub connect_call_datetime_setup { 1 }
 +
  sub _do_query {
    my ($self, $action) = @_;
  
@@@ -1461,6 -1296,7 +1461,7 @@@ sub _adjust_select_args_for_limited_pre
    $self->throw_exception ('Prefetch with limit (rows/offset) is not supported on resultsets with a custom from attribute')
      if (ref $from ne 'ARRAY');
  
    # separate attributes
    my $sub_attrs = { %$attrs };
    delete $attrs->{$_} for qw/where bind rows offset/;
      ];
    }
  
+   # mangle {from}
+   $from = [ @$from ];
+   my $select_root = shift @$from;
+   my @outer_from = @$from;
  
-   # mangle the head of the {from}
-   my $self_ident = shift @$from;
+   my %inner_joins;
    my %join_info = map { $_->[0]{-alias} => $_->[0] } (@$from);
  
-   my (%inner_joins);
+   # in complex search_related chains $alias may *not* be 'me'
+   # so always include it in the inner join, and also shift away
+   # from the outer stack, so that the two datasets actually do
+   # meet
+   if ($select_root->{-alias} ne $alias) {
+     $inner_joins{$alias} = 1;
+     while (@outer_from && $outer_from[0][0]{-alias} ne $alias) {
+       shift @outer_from;
+     }
+     if (! @outer_from) {
+       $self->throw_exception ("Unable to find '$alias' in the {from} stack, something is wrong");
+     }
+     shift @outer_from; # the new subquery will represent this alias, so get rid of it
+   }
  
    # decide which parts of the join will remain on the inside
    #
    }
  
    # construct the inner $from for the subquery
-   my $inner_from = [ $self_ident ];
-   if (keys %inner_joins) {
-     for my $j (@$from) {
-       push @$inner_from, $j if $inner_joins{$j->[0]{-alias}};
-     }
+   my $inner_from = [ $select_root ];
+   for my $j (@$from) {
+     push @$inner_from, $j if $inner_joins{$j->[0]{-alias}};
+   }
  
-     # if a multi-type join was needed in the subquery ("multi" is indicated by
-     # presence in {collapse}) - add a group_by to simulate the collapse in the subq
-     for my $alias (keys %inner_joins) {
+   # if a multi-type join was needed in the subquery ("multi" is indicated by
+   # presence in {collapse}) - add a group_by to simulate the collapse in the subq
  
-       # the dot comes from some weirdness in collapse
-       # remove after the rewrite
-       if ($attrs->{collapse}{".$alias"}) {
-         $sub_attrs->{group_by} = $sub_select;
-         last;
-       }
+   for my $alias (keys %inner_joins) {
+     # the dot comes from some weirdness in collapse
+     # remove after the rewrite
+     if ($attrs->{collapse}{".$alias"}) {
+       $sub_attrs->{group_by} = $sub_select;
+       last;
      }
    }
  
      $sub_attrs
    );
  
-   # put it back in $from
-   unshift @$from, { $alias => $subq };
+   # put it in the new {from}
+   unshift @outer_from, { $alias => $subq };
  
    # This is totally horrific - the $where ends up in both the inner and outer query
    # Unfortunately not much can be done until SQLA2 introspection arrives
    #
    # OTOH it can be seen as a plus: <ash> (notes that this query would make a DBA cry ;)
-   return ($from, $select, $where, $attrs);
+   return (\@outer_from, $select, $where, $attrs);
  }
  
  sub _resolve_ident_sources {
    return $alias2source;
  }
  
- sub count {
-   my ($self, $source, $attrs) = @_;
-   my $tmp_attrs = { %$attrs };
-   # take off any limits, record_filter is cdbi, and no point of ordering a count
-   delete $tmp_attrs->{$_} for (qw/select as rows offset order_by record_filter/);
-   # overwrite the selector
-   $tmp_attrs->{select} = { count => '*' };
-   my $tmp_rs = $source->resultset_class->new($source, $tmp_attrs);
-   my ($count) = $tmp_rs->cursor->next;
-   # if the offset/rows attributes are still present, we did not use
-   # a subquery, so we need to make the calculations in software
-   $count -= $attrs->{offset} if $attrs->{offset};
-   $count = $attrs->{rows} if $attrs->{rows} and $attrs->{rows} < $count;
-   $count = 0 if ($count < 0);
-   return $count;
- }
- sub count_grouped {
-   my ($self, $source, $attrs) = @_;
-   # copy for the subquery, we need to do some adjustments to it too
-   my $sub_attrs = { %$attrs };
-   # these can not go in the subquery, and there is no point of ordering it
-   delete $sub_attrs->{$_} for qw/collapse select as order_by/;
-   # if we prefetch, we group_by primary keys only as this is what we would get out of the rs via ->next/->all
-   # simply deleting group_by suffices, as the code below will re-fill it
-   # Note: we check $attrs, as $sub_attrs has collapse deleted
-   if (ref $attrs->{collapse} and keys %{$attrs->{collapse}} ) {
-     delete $sub_attrs->{group_by};
-   }
-   $sub_attrs->{group_by} ||= [ map { "$attrs->{alias}.$_" } ($source->primary_columns) ];
-   $sub_attrs->{select} = $self->_grouped_count_select ($source, $sub_attrs);
-   $attrs->{from} = [{
-     count_subq => $source->resultset_class->new ($source, $sub_attrs )->as_query
-   }];
-   # the subquery replaces this
-   delete $attrs->{$_} for qw/where bind collapse group_by having having_bind rows offset/;
-   return $self->count ($source, $attrs);
+ # Returns a counting SELECT for a simple count
+ # query. Abstracted so that a storage could override
+ # this to { count => 'firstcol' } or whatever makes
+ # sense as a performance optimization
+ sub _count_select {
+   #my ($self, $source, $rs_attrs) = @_;
+   return { count => '*' };
  }
  
+ # Returns a SELECT which will end up in the subselect
+ # There may or may not be a group_by, as the subquery
+ # might have been called to accomodate a limit
  #
- # Returns a SELECT to go with a supplied GROUP BY
- # (caled by count_grouped so a group_by is present)
- # Most databases expect them to match, but some
- # choke in various ways.
+ # Most databases would be happy with whatever ends up
+ # here, but some choke in various ways.
  #
- sub _grouped_count_select {
-   my ($self, $source, $rs_args) = @_;
-   return $rs_args->{group_by};
+ sub _subq_count_select {
+   my ($self, $source, $rs_attrs) = @_;
+   return $rs_attrs->{group_by} if $rs_attrs->{group_by};
+   my @pcols = map { join '.', $rs_attrs->{alias}, $_ } ($source->primary_columns);
+   return @pcols ? \@pcols : [ 1 ];
  }
  
  sub source_bind_attributes {
    my ($self, $source) = @_;
-   
    my $bind_attributes;
    foreach my $column ($source->columns) {
-   
      my $data_type = $source->column_info($column)->{data_type} || '';
      $bind_attributes->{$column} = $self->bind_attribute_by_data_type($data_type)
       if $data_type;