Caelum was right to make _get_dbh private - reverting (and some code refactoring)
[dbsrgits/DBIx-Class.git] / lib / DBIx / Class / Storage / DBI / MSSQL.pm
index fe51f1b..b0da553 100644 (file)
@@ -8,6 +8,10 @@ use mro 'c3';
 
 use List::Util();
 
+__PACKAGE__->mk_group_accessors(simple => qw/
+  _identity _identity_method
+/);
+
 __PACKAGE__->sql_maker_class('DBIx::Class::SQLAHacks::MSSQL');
 
 sub insert_bulk {
@@ -26,17 +30,53 @@ sub insert_bulk {
 
   if ($identity_insert) {
     my $table = $source->from;
-    $self->dbh->do("SET IDENTITY_INSERT $table ON");
+    $self->_get_dbh->do("SET IDENTITY_INSERT $table ON");
   }
 
   $self->next::method(@_);
 
   if ($identity_insert) {
     my $table = $source->from;
-    $self->dbh->do("SET IDENTITY_INSERT $table OFF");
+    $self->_get_dbh->do("SET IDENTITY_INSERT $table OFF");
   }
 }
 
+# support MSSQL GUID column types
+
+sub insert {
+  my $self = shift;
+  my ($source, $to_insert) = @_;
+
+  my $updated_cols = {};
+
+  my %guid_cols;
+  my @pk_cols = $source->primary_columns;
+  my %pk_cols;
+  @pk_cols{@pk_cols} = ();
+
+  my @pk_guids = grep {
+    $source->column_info($_)->{data_type} =~ /^uniqueidentifier/i
+  } @pk_cols;
+
+  my @auto_guids = grep {
+    $source->column_info($_)->{data_type} =~ /^uniqueidentifier/i
+    &&
+    $source->column_info($_)->{auto_nextval}
+  } grep { not exists $pk_cols{$_} } $source->columns;
+
+  my @get_guids_for =
+    grep { not exists $to_insert->{$_} } (@pk_guids, @auto_guids);
+
+  for my $guid_col (@get_guids_for) {
+    my ($new_guid) = $self->_get_dbh->selectrow_array('SELECT NEWID()');
+    $updated_cols->{$guid_col} = $to_insert->{$guid_col} = $new_guid;
+  }
+
+  $updated_cols = { %$updated_cols, %{ $self->next::method(@_) } };
+
+  return $updated_cols;
+}
+
 sub _prep_for_execute {
   my $self = shift;
   my ($op, $extra_bind, $ident, $args) = @_;
@@ -44,10 +84,10 @@ sub _prep_for_execute {
 # cast MONEY values properly
   if ($op eq 'insert' || $op eq 'update') {
     my $fields = $args->[0];
-    my $col_info = $self->_resolve_column_info($ident, [keys %$fields]);
 
     for my $col (keys %$fields) {
-      if ($col_info->{$col}{data_type} =~ /^money\z/i) {
+      # $ident is a result source object with INSERT/UPDATE ops
+      if ($ident->column_info ($col)->{data_type} =~ /^money\z/i) {
         my $val = $fields->{$col};
         $fields->{$col} = \['CAST(? AS MONEY)', [ $col => $val ]];
       }
@@ -77,16 +117,45 @@ sub _execute {
   my ($op) = @_;
 
   my ($rv, $sth, @bind) = $self->dbh_do($self->can('_dbh_execute'), @_);
+
   if ($op eq 'insert') {
-    $self->{_scope_identity} = $sth->fetchrow_array;
+
+    # this should bring back the result of SELECT SCOPE_IDENTITY() we tacked
+    # on in _prep_for_execute above
+    my ($identity) = $sth->fetchrow_array;
+
+    # SCOPE_IDENTITY failed, but we can do something else
+    if ( (! $identity) && $self->_identity_method) {
+      ($identity) = $self->_dbh->selectrow_array(
+        'select ' . $self->_identity_method
+      );
+    }
+
+    $self->_identity($identity);
     $sth->finish;
   }
 
   return wantarray ? ($rv, $sth, @bind) : $rv;
 }
 
+sub last_insert_id { shift->_identity }
+
+# savepoint syntax is the same as in Sybase ASE
+
+sub _svp_begin {
+  my ($self, $name) = @_;
+
+  $self->_get_dbh->do("SAVE TRANSACTION $name");
+}
 
-sub last_insert_id { shift->{_scope_identity} }
+# A new SAVE TRANSACTION with the same name releases the previous one.
+sub _svp_release { 1 }
+
+sub _svp_rollback {
+  my ($self, $name) = @_;
+
+  $self->_get_dbh->do("ROLLBACK TRANSACTION $name");
+}
 
 sub build_datetime_parser {
   my $self = shift;
@@ -131,6 +200,16 @@ be called is the same execute statement, not just the same connection.
 So, this implementation appends a SELECT SCOPE_IDENTITY() statement
 onto each INSERT to accommodate that requirement.
 
+C<SELECT @@IDENTITY> can also be used by issuing:
+
+  $self->_identity_method('@@identity');
+
+it will only be used if SCOPE_IDENTITY() fails.
+
+This is more dangerous, as inserting into a table with an on insert trigger that
+inserts into another table with an identity will give erroneous results on
+recent versions of SQL Server.
+
 =head1 AUTHOR
 
 See L<DBIx::Class/CONTRIBUTORS>.