X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FDBIx%2FClass%2FStorage%2FDBI%2FMSSQL.pm;h=9a49a42dbe2bfdd555e4a295be7e4d375692d621;hb=HEAD;hp=171c17a6a1628abdb499a67c0f55397a00c711c7;hpb=75d079145a507a0e5ff89b2676d383f4fd1a5511;p=dbsrgits%2FDBIx-Class.git diff --git a/lib/DBIx/Class/Storage/DBI/MSSQL.pm b/lib/DBIx/Class/Storage/DBI/MSSQL.pm index 171c17a..9a49a42 100644 --- a/lib/DBIx/Class/Storage/DBI/MSSQL.pm +++ b/lib/DBIx/Class/Storage/DBI/MSSQL.pm @@ -1,39 +1,357 @@ package DBIx::Class::Storage::DBI::MSSQL; - + use strict; use warnings; - -use base qw/DBIx::Class::Storage::DBI/; - -# __PACKAGE__->load_components(qw/PK::Auto/); - -sub last_insert_id { - my( $id ) = $_[0]->_dbh->selectrow_array('SELECT @@IDENTITY' ); - return $id; -} - + +use base qw/ + DBIx::Class::Storage::DBI::UniqueIdentifier + DBIx::Class::Storage::DBI::IdentityInsert +/; +use mro 'c3'; + +use DBIx::Class::_Util qw( dbic_internal_try dbic_internal_catch sigwarn_silencer ); +use namespace::clean; + +__PACKAGE__->mk_group_accessors(simple => qw/ + _identity _identity_method _no_scope_identity_query +/); + +__PACKAGE__->sql_maker_class('DBIx::Class::SQLMaker::MSSQL'); + +__PACKAGE__->sql_quote_char([qw/[ ]/]); + +__PACKAGE__->datetime_parser_type ( + 'DBIx::Class::Storage::DBI::MSSQL::DateTime::Format' +); + +__PACKAGE__->new_guid('NEWID()'); + +sub _prep_for_execute { + my $self = shift; + my ($op, $ident, $args) = @_; + +# cast MONEY values properly + if ($op eq 'insert' || $op eq 'update') { + my $fields = $args->[0]; + + my $colinfo = $ident->columns_info([keys %$fields]); + + for my $col (keys %$fields) { + # $ident is a result source object with INSERT/UPDATE ops + if ( + $colinfo->{$col}{data_type} + && + $colinfo->{$col}{data_type} =~ /^money\z/i + ) { + my $val = $fields->{$col}; + $fields->{$col} = \['CAST(? AS MONEY)', [ $col => $val ]]; + } + } + } + + my ($sql, $bind) = $self->next::method (@_); + + # SELECT SCOPE_IDENTITY only works within a statement scope. We + # must try to always use this particular idiom first, as it is the + # only one that guarantees retrieving the correct id under high + # concurrency. When this fails we will fall back to whatever secondary + # retrieval method is specified in _identity_method, but at this + # point we don't have many guarantees we will get what we expected. + # http://msdn.microsoft.com/en-us/library/ms190315.aspx + # http://davidhayden.com/blog/dave/archive/2006/01/17/2736.aspx + if ($self->_perform_autoinc_retrieval and not $self->_no_scope_identity_query) { + $sql .= "\nSELECT SCOPE_IDENTITY()"; + } + + return ($sql, $bind); +} + +sub _execute { + my $self = shift; + + # always list ctx - we need the $sth + my ($rv, $sth, @bind) = $self->next::method(@_); + + if ($self->_perform_autoinc_retrieval) { + + # attempt to bring back the result of SELECT SCOPE_IDENTITY() we tacked + # on in _prep_for_execute above + my $identity; + + # we didn't even try on ftds + unless ($self->_no_scope_identity_query) { + ($identity) = dbic_internal_try { $sth->fetchrow_array }; + $sth->finish; + } + + # 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); + } + + return wantarray ? ($rv, $sth, @bind) : $rv; +} + +sub last_insert_id { shift->_identity } + +# +# MSSQL is retarded wrt ordered subselects. One needs to add a TOP +# to *all* subqueries, but one also *can't* use TOP 100 PERCENT +# http://sqladvice.com/forums/permalink/18496/22931/ShowThread.aspx#22931 +# +sub _select_args_to_query { + #my ($self, $ident, $select, $cond, $attrs) = @_; + my $self = shift; + my $attrs = $_[3]; + + my $sql_bind = $self->next::method (@_); + + # see if this is an ordered subquery + if ( + $$sql_bind->[0] !~ /^ \s* \( \s* SELECT \s+ TOP \s+ \d+ \s+ /xi + and + scalar $self->_extract_order_criteria ($attrs->{order_by}) + ) { + $self->throw_exception( + 'An ordered subselect encountered - this is not safe! Please see "Ordered Subselects" in DBIx::Class::Storage::DBI::MSSQL' + ) unless $attrs->{unsafe_subselect_ok}; + + $$sql_bind->[0] =~ s/^ \s* \( \s* SELECT (?=\s) / '(SELECT TOP ' . $self->sql_maker->__max_int /exi; + } + + $sql_bind; +} + + +# savepoint syntax is the same as in Sybase ASE + +sub _exec_svp_begin { + my ($self, $name) = @_; + + $self->_dbh->do("SAVE TRANSACTION $name"); +} + +# A new SAVE TRANSACTION with the same name releases the previous one. +sub _exec_svp_release { 1 } + +sub _exec_svp_rollback { + my ($self, $name) = @_; + + $self->_dbh->do("ROLLBACK TRANSACTION $name"); +} + +sub sqlt_type { 'SQLServer' } + +sub sql_limit_dialect { + my $self = shift; + + my $supports_rno = 0; + + if (exists $self->_server_info->{normalized_dbms_version}) { + $supports_rno = 1 if $self->_server_info->{normalized_dbms_version} >= 9; + } + else { + # User is connecting via DBD::Sybase and has no permission to run + # stored procedures like xp_msver, or version detection failed for some + # other reason. + # So, we use a query to check if RNO is implemented. + dbic_internal_try { + $self->_get_dbh->selectrow_array('SELECT row_number() OVER (ORDER BY rand())'); + $supports_rno = 1; + }; + } + + return $supports_rno ? 'RowNumberOver' : 'Top'; +} + +sub _ping { + my $self = shift; + + my $dbh = $self->_dbh or return 0; + + dbic_internal_try { + local $dbh->{RaiseError} = 1; + local $dbh->{PrintError} = 0; + + $dbh->do('select 1'); + 1; + } + dbic_internal_catch { + # MSSQL is *really* annoying wrt multiple active resultsets, + # and this may very well be the reason why the _ping failed + # + # Proactively disconnect, while hiding annoying warnings if the case + # + # The callchain is: + # < check basic retryability prerequisites (e.g. no txn) > + # ->retry_handler + # ->storage->connected() + # ->ping + # So if we got here with the in_handler bit set - we won't break + # anything by a disconnect + if( $self->{_in_do_block_retry_handler} ) { + local $SIG{__WARN__} = sigwarn_silencer qr/disconnect invalidates .+? active statement/; + $self->disconnect; + } + + # RV of _ping itself + 0; + }; +} + +package # hide from PAUSE + DBIx::Class::Storage::DBI::MSSQL::DateTime::Format; + +my $datetime_format = '%Y-%m-%d %H:%M:%S.%3N'; # %F %T +my $smalldatetime_format = '%Y-%m-%d %H:%M:%S'; + +my ($datetime_parser, $smalldatetime_parser); + +sub parse_datetime { + shift; + require DateTime::Format::Strptime; + $datetime_parser ||= DateTime::Format::Strptime->new( + pattern => $datetime_format, + on_error => 'croak', + ); + return $datetime_parser->parse_datetime(shift); +} + +sub format_datetime { + shift; + require DateTime::Format::Strptime; + $datetime_parser ||= DateTime::Format::Strptime->new( + pattern => $datetime_format, + on_error => 'croak', + ); + return $datetime_parser->format_datetime(shift); +} + +sub parse_smalldatetime { + shift; + require DateTime::Format::Strptime; + $smalldatetime_parser ||= DateTime::Format::Strptime->new( + pattern => $smalldatetime_format, + on_error => 'croak', + ); + return $smalldatetime_parser->parse_datetime(shift); +} + +sub format_smalldatetime { + shift; + require DateTime::Format::Strptime; + $smalldatetime_parser ||= DateTime::Format::Strptime->new( + pattern => $smalldatetime_format, + on_error => 'croak', + ); + return $smalldatetime_parser->format_datetime(shift); +} + 1; - + =head1 NAME - -DBIx::Class::Storage::DBI::MSSQL - Automatic primary key class for MSSQL - + +DBIx::Class::Storage::DBI::MSSQL - Base Class for Microsoft SQL Server support +in DBIx::Class + =head1 SYNOPSIS - - # In your table classes - __PACKAGE__->load_components(qw/PK::Auto Core/); - __PACKAGE__->set_primary_key('id'); - -=head1 DESCRIPTION - -This class implements autoincrements for MSSQL. - -=head1 AUTHORS - -Brian Cassidy - -=head1 LICENSE - -You may distribute this code under the same terms as Perl itself. - -=cut + +This is the base class for Microsoft SQL Server support, used by +L and +L. + +=head1 IMPLEMENTATION NOTES + +=head2 IDENTITY information + +Microsoft SQL Server supports three methods of retrieving the IDENTITY +value for inserted row: IDENT_CURRENT, @@IDENTITY, and SCOPE_IDENTITY(). +SCOPE_IDENTITY is used here because it is the safest. However, it must +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