deploy debugging and drop table statements
[dbsrgits/DBIx-Class.git] / lib / DBIx / Class / Storage / DBI.pm
index 37547d2..f6e682d 100644 (file)
@@ -1,11 +1,14 @@
 package DBIx::Class::Storage::DBI;
 
+use base 'DBIx::Class::Storage';
+
 use strict;
 use warnings;
 use DBI;
 use SQL::Abstract::Limit;
 use DBIx::Class::Storage::DBI::Cursor;
 use IO::File;
+use Carp::Clan qw/DBIx::Class/;
 
 BEGIN {
 
@@ -16,8 +19,10 @@ use base qw/SQL::Abstract::Limit/;
 sub select {
   my ($self, $table, $fields, $where, $order, @rest) = @_;
   @rest = (-1) unless defined $rest[0];
-  $self->SUPER::select($table, $self->_recurse_fields($fields), 
-                         $where, $order, @rest);
+  local $self->{having_bind} = [];
+  my ($sql, @ret) = $self->SUPER::select($table,
+                      $self->_recurse_fields($fields), $where, $order, @rest);
+  return wantarray ? ($sql, @ret, @{$self->{having_bind}}) : $sql;
 }
 
 sub _emulate_limit {
@@ -48,20 +53,35 @@ sub _recurse_fields {
 sub _order_by {
   my $self = shift;
   my $ret = '';
+  my @extra;
   if (ref $_[0] eq 'HASH') {
     if (defined $_[0]->{group_by}) {
       $ret = $self->_sqlcase(' group by ')
                .$self->_recurse_fields($_[0]->{group_by});
     }
+    if (defined $_[0]->{having}) {
+      my $frag;
+      ($frag, @extra) = $self->_recurse_where($_[0]->{having});
+      push(@{$self->{having_bind}}, @extra);
+      $ret .= $self->_sqlcase(' having ').$frag;
+    }
     if (defined $_[0]->{order_by}) {
       $ret .= $self->SUPER::_order_by($_[0]->{order_by});
     }
+  } elsif(ref $_[0] eq 'SCALAR') {
+    $ret = $self->_sqlcase(' order by ').${ $_[0] };
   } else {
     $ret = $self->SUPER::_order_by(@_);
   }
   return $ret;
 }
 
+sub _order_directions {
+  my ($self, $order) = @_;
+  $order = $order->{order_by} if ref $order eq 'HASH';
+  return $self->SUPER::_order_directions($order);
+}
+
 sub _table {
   my ($self, $from) = @_;
   if (ref $from eq 'ARRAY') {
@@ -129,17 +149,69 @@ sub _join_condition {
 sub _quote {
   my ($self, $label) = @_;
   return '' unless defined $label;
+  return "*" if $label eq '*';
+  return $label unless $self->{quote_char};
+  if(ref $self->{quote_char} eq "ARRAY"){
+    return $self->{quote_char}->[0] . $label . $self->{quote_char}->[1]
+      if !defined $self->{name_sep};
+    my $sep = $self->{name_sep};
+    return join($self->{name_sep},
+        map { $self->{quote_char}->[0] . $_ . $self->{quote_char}->[1]  }
+       split(/\Q$sep\E/,$label));
+  }
   return $self->SUPER::_quote($label);
 }
 
+sub _RowNum {
+   my $self = shift;
+   my $c;
+   $_[0] =~ s/SELECT (.*?) FROM/
+     'SELECT '.join(', ', map { $_.' AS col'.++$c } split(', ', $1)).' FROM'/e;
+   $self->SUPER::_RowNum(@_);
+}
+
+# Accessor for setting limit dialect. This is useful
+# for JDBC-bridge among others where the remote SQL-dialect cannot
+# be determined by the name of the driver alone.
+#
+sub limit_dialect {
+    my $self = shift;
+    $self->{limit_dialect} = shift if @_;
+    return $self->{limit_dialect};
+}
+
+sub quote_char {
+    my $self = shift;
+    $self->{quote_char} = shift if @_;
+    return $self->{quote_char};
+}
+
+sub name_sep {
+    my $self = shift;
+    $self->{name_sep} = shift if @_;
+    return $self->{name_sep};
+}
+
+
+
+
+package DBIx::Class::Storage::DBI::DebugCallback;
+
+sub print {
+  my ($self, $string) = @_;
+  $string =~ m/^(\w+)/;
+  ${$self}->($1, $string);
+}
+
 } # End of BEGIN block
 
 use base qw/DBIx::Class/;
 
-__PACKAGE__->load_components(qw/Exception AccessorGroup/);
+__PACKAGE__->load_components(qw/AccessorGroup/);
 
 __PACKAGE__->mk_group_accessors('simple' =>
-  qw/connect_info _dbh _sql_maker debug debugfh cursor on_connect_do transaction_depth/);
+  qw/connect_info _dbh _sql_maker _connection_pid debug debugfh cursor
+     on_connect_do transaction_depth/);
 
 sub new {
   my $new = bless({}, ref $_[0] || $_[0]);
@@ -147,7 +219,7 @@ sub new {
   $new->transaction_depth(0);
   if (defined($ENV{DBIX_CLASS_STORAGE_DBI_DEBUG}) &&
      ($ENV{DBIX_CLASS_STORAGE_DBI_DEBUG} =~ /=(.+)$/)) {
-    $new->debugfh(IO::File->new($1, 'w')||die "Cannot open trace file $1");
+    $new->debugfh(IO::File->new($1, 'w')) || $new->throw_exception("Cannot open trace file $1");
   } else {
     $new->debugfh(IO::File->new('>&STDERR'));
   }
@@ -155,6 +227,11 @@ sub new {
   return $new;
 }
 
+sub throw_exception {
+  my ($self, $msg) = @_;
+  croak($msg);
+}
+
 =head1 NAME 
 
 DBIx::Class::Storage::DBI - DBI storage handler
@@ -185,14 +262,54 @@ should be an IO::Handle compatible object (only the C<print> method is
 used).  Initially set to be STDERR - although see information on the
 L<DBIX_CLASS_STORAGE_DBI_DEBUG> environment variable.
 
+=head2 debugcb
+
+Sets a callback to be executed each time a statement is run; takes a sub
+reference. Overrides debugfh. Callback is executed as $sub->($op, $info)
+where $op is SELECT/INSERT/UPDATE/DELETE and $info is what would normally
+be printed.
+
 =cut
 
-sub dbh {
+sub debugcb {
+  my ($self, $cb) = @_;
+  my $cb_obj = bless(\$cb, 'DBIx::Class::Storage::DBI::DebugCallback');
+  $self->debugfh($cb_obj);
+}
+
+sub disconnect {
+  my ($self) = @_;
+
+  if( $self->connected ) {
+    $self->_dbh->rollback unless $self->_dbh->{AutoCommit};
+    $self->_dbh->disconnect;
+    $self->_dbh(undef);
+  }
+}
+
+sub connected {
   my ($self) = @_;
+
   my $dbh;
-  unless (($dbh = $self->_dbh) && $dbh->FETCH('Active') && $dbh->ping) {
+  (($dbh = $self->_dbh) && $dbh->FETCH('Active') && $dbh->ping)
+}
+
+sub ensure_connected {
+  my ($self) = @_;
+
+  unless ($self->connected) {
     $self->_populate_dbh;
   }
+}
+
+sub dbh {
+  my ($self) = @_;
+
+  if($self->_connection_pid && $self->_connection_pid != $$) {
+      $self->_dbh->{InactiveDestroy} = 1;
+      $self->_dbh(undef)
+  }
+  $self->ensure_connected;
   return $self->_dbh;
 }
 
@@ -208,27 +325,57 @@ sub _populate_dbh {
   my ($self) = @_;
   my @info = @{$self->connect_info || []};
   $self->_dbh($self->_connect(@info));
-
+  my $driver = $self->_dbh->{Driver}->{Name};
+  eval "require DBIx::Class::Storage::DBI::${driver}";
+  unless ($@) {
+    bless $self, "DBIx::Class::Storage::DBI::${driver}";
+  }
   # if on-connect sql statements are given execute them
   foreach my $sql_statement (@{$self->on_connect_do || []}) {
     $self->_dbh->do($sql_statement);
   }
+
+  $self->_connection_pid($$);
 }
 
 sub _connect {
   my ($self, @info) = @_;
-  return DBI->connect(@info);
+
+  my ($old_connect_via, $dbh);
+
+  if ($INC{'Apache/DBI.pm'} && $ENV{MOD_PERL}) {
+      $old_connect_via = $DBI::connect_via;
+      $DBI::connect_via = 'connect';
+  }
+
+  if(ref $info[0] eq 'CODE') {
+      $dbh = &{$info[0]};
+  }
+  else {
+      $dbh = DBI->connect(@info);
+  }
+
+  $DBI::connect_via = $old_connect_via if $old_connect_via;
+
+  $self->throw_exception("DBI Connection failed: $DBI::errstr")
+      unless $dbh;
+
+  $dbh;
 }
 
 =head2 txn_begin
 
 Calls begin_work on the current dbh.
 
+See L<DBIx::Class::Schema> for the txn_do() method, which allows for
+an entire code block to be executed transactionally.
+
 =cut
 
 sub txn_begin {
   my $self = shift;
-  $self->dbh->begin_work if $self->{transaction_depth}++ == 0 and $self->dbh->{AutoCommit};
+  $self->dbh->begin_work
+    if $self->{transaction_depth}++ == 0 and $self->dbh->{AutoCommit};
 }
 
 =head2 txn_commit
@@ -249,17 +396,32 @@ sub txn_commit {
 
 =head2 txn_rollback
 
-Issues a rollback against the current dbh.
+Issues a rollback against the current dbh. A nested rollback will
+throw a L<DBIx::Class::Storage::NESTED_ROLLBACK_EXCEPTION> exception,
+which allows the rollback to propagate to the outermost transaction.
 
 =cut
 
 sub txn_rollback {
   my $self = shift;
-  if ($self->{transaction_depth} == 0) {
-    $self->dbh->rollback unless $self->dbh->{AutoCommit};
-  }
-  else {
-    --$self->{transaction_depth} == 0 ? $self->dbh->rollback : die $@;    
+
+  eval {
+    if ($self->{transaction_depth} == 0) {
+      $self->dbh->rollback unless $self->dbh->{AutoCommit};
+    }
+    else {
+      --$self->{transaction_depth} == 0 ?
+        $self->dbh->rollback :
+       die DBIx::Class::Storage::NESTED_ROLLBACK_EXCEPTION->new;
+    }
+  };
+
+  if ($@) {
+    my $error = $@;
+    my $exception_class = "DBIx::Class::Storage::NESTED_ROLLBACK_EXCEPTION";
+    $error =~ /$exception_class/ and $self->throw_exception($error);
+    $self->{transaction_depth} = 0;          # ensure that a failed rollback
+    $self->throw_exception($error);          # resets the transaction depth
   }
 }
 
@@ -267,16 +429,25 @@ sub _execute {
   my ($self, $op, $extra_bind, $ident, @args) = @_;
   my ($sql, @bind) = $self->sql_maker->$op($ident, @args);
   unshift(@bind, @$extra_bind) if $extra_bind;
-  $self->debugfh->print("$sql: @bind\n") if $self->debug;
+  if ($self->debug) {
+      my @debug_bind = map { defined $_ ? qq{`$_'} : q{`NULL'} } @bind;
+      $self->debugfh->print("$sql: " . join(', ', @debug_bind) . "\n");
+  }
   my $sth = $self->sth($sql,$op);
+  $self->throw_exception("no sth generated via sql: $sql") unless $sth;
   @bind = map { ref $_ ? ''.$_ : $_ } @bind; # stringify args
-  my $rv = $sth->execute(@bind);
+  my $rv;
+  if ($sth) {  
+    $rv = $sth->execute(@bind) or $self->throw_exception("Error executing '$sql': " . $sth->errstr);
+  } else { 
+    $self->throw_exception("'$sql' did not generate a statement.");
+  }
   return (wantarray ? ($rv, $sth, @bind) : $rv);
 }
 
 sub insert {
   my ($self, $ident, $to_insert) = @_;
-  $self->throw( "Couldn't insert ".join(', ', map "$_ => $to_insert->{$_}", keys %$to_insert)." into ${ident}" )
+  $self->throw_exception( "Couldn't insert ".join(', ', map "$_ => $to_insert->{$_}", keys %$to_insert)." into ${ident}" )
     unless ($self->_execute('insert' => [], $ident, $to_insert));
   return $to_insert;
 }
@@ -295,8 +466,9 @@ sub _select {
   if (ref $condition eq 'SCALAR') {
     $order = $1 if $$condition =~ s/ORDER BY (.*)$//i;
   }
-  if (exists $attrs->{group_by}) {
+  if (exists $attrs->{group_by} || $attrs->{having}) {
     $order = { group_by => $attrs->{group_by},
+               having => $attrs->{having},
                ($order ? (order_by => $order) : ()) };
   }
   my @args = ('select', $attrs->{bind}, $ident, $select, $condition, $order);
@@ -315,10 +487,14 @@ sub select {
   return $self->cursor->new($self, \@_, $attrs);
 }
 
+# Need to call finish() to work round broken DBDs
+
 sub select_single {
   my $self = shift;
   my ($rv, $sth, @bind) = $self->_select(@_);
-  return $sth->fetchrow_array;
+  my @row = $sth->fetchrow_array;
+  $sth->finish();
+  return @row;
 }
 
 sub sth {
@@ -346,7 +522,7 @@ sub columns_info_for {
             $column_info{is_nullable} = $info->{NULLABLE};
             $result{$info->{COLUMN_NAME}} = \%column_info;
         }
-    }else{
+    } else {
         my $sth = $self->dbh->prepare("SELECT * FROM $table WHERE 1=0");
         $sth->execute;
         my @columns = @{$sth->{NAME}};
@@ -357,6 +533,55 @@ sub columns_info_for {
     return \%result;
 }
 
+sub last_insert_id {
+  my ($self, $row) = @_;
+    
+  return $self->dbh->func('last_insert_rowid');
+
+}
+
+sub sqlt_type { shift->dbh->{Driver}->{Name} }
+
+sub deployment_statements {
+  my ($self, $schema, $type) = @_;
+  $type ||= $self->sqlt_type;
+  eval "use SQL::Translator";
+  $self->throw_exception("Can't deploy without SQL::Translator: $@") if $@;
+  eval "use SQL::Translator::Parser::DBIx::Class;";
+  $self->throw_exception($@) if $@; 
+  eval "use SQL::Translator::Producer::${type};";
+  $self->throw_exception($@) if $@;
+  my $tr = SQL::Translator->new(
+      # Print debug info
+      debug               => 1,
+      # Print Parse::RecDescent trace
+      trace               => 0,
+      # Don't include comments in output
+      no_comments         => 1,
+      # Print name mutations, conflicts
+      show_warnings       => 0,
+      # Add "drop table" statements
+      add_drop_table      => 1,
+      # Validate schema object
+      validate            => 1,
+      # Make all table names CAPS in producers which support this option
+      format_table_name   => sub {my $tablename = shift; return uc($tablename)},
+  );
+
+  SQL::Translator::Parser::DBIx::Class::parse( $tr, $schema );
+  return "SQL::Translator::Producer::${type}"->can('produce')->($tr);
+}
+
+sub deploy {
+  my ($self, $schema, $type) = @_;
+  foreach(split(";\n", $self->deployment_statements($schema, $type))) {
+      $self->debugfh->print("$_\n") if $self->debug;
+         $self->dbh->do($_) or warn "SQL was:\n $_";
+  } 
+}
+
+sub DESTROY { shift->disconnect }
+
 1;
 
 =head1 ENVIRONMENT VARIABLES