X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FDBIx%2FClass%2FStorage%2FDBI%2FMSSQL.pm;h=a147091eec8046d5244507edf5b90b56cdfb558e;hb=3e4a74aa6eeb3ad2b9d5979ec7d41adc6efe9be2;hp=fe51f1b85a4b6d61594236299d1ea03c0f4ed285;hpb=5a77aa8ba52c50bdc4291ca22433f9c315234ad2;p=dbsrgits%2FDBIx-Class.git diff --git a/lib/DBIx/Class/Storage/DBI/MSSQL.pm b/lib/DBIx/Class/Storage/DBI/MSSQL.pm index fe51f1b..a147091 100644 --- a/lib/DBIx/Class/Storage/DBI/MSSQL.pm +++ b/lib/DBIx/Class/Storage/DBI/MSSQL.pm @@ -3,51 +3,63 @@ package DBIx::Class::Storage::DBI::MSSQL; use strict; use warnings; -use base qw/DBIx::Class::Storage::DBI::AmbiguousGlob DBIx::Class::Storage::DBI/; +use base qw/ + DBIx::Class::Storage::DBI::UniqueIdentifier + DBIx::Class::Storage::DBI::IdentityInsert +/; use mro 'c3'; -use List::Util(); +use Try::Tiny; +use List::Util 'first'; +use namespace::clean; -__PACKAGE__->sql_maker_class('DBIx::Class::SQLAHacks::MSSQL'); +__PACKAGE__->mk_group_accessors(simple => qw/ + _identity _identity_method _no_scope_identity_query +/); -sub insert_bulk { - my $self = shift; - my ($source, $cols, $data) = @_; +__PACKAGE__->sql_maker_class('DBIx::Class::SQLMaker::MSSQL'); - my $identity_insert = 0; +__PACKAGE__->sql_quote_char([qw/[ ]/]); - COLUMNS: - foreach my $col (@{$cols}) { - if ($source->column_info($col)->{is_auto_increment}) { - $identity_insert = 1; - last COLUMNS; - } - } +__PACKAGE__->datetime_parser_type ( + 'DBIx::Class::Storage::DBI::MSSQL::DateTime::Format' +); - if ($identity_insert) { - my $table = $source->from; - $self->dbh->do("SET IDENTITY_INSERT $table ON"); - } +__PACKAGE__->new_guid('NEWID()'); - $self->next::method(@_); +sub __sql_server_x_or_higher { + my ($self, $version) = @_; - if ($identity_insert) { - my $table = $source->from; - $self->dbh->do("SET IDENTITY_INSERT $table OFF"); + if (exists $_[0]->_server_info->{normalized_dbms_version}) { + if ($_[0]->_server_info->{normalized_dbms_version} >= $version) { + return 1 + } else { + return 0 + } } + return undef; } +sub _sql_server_2005_or_higher { shift->__sql_server_x_or_higher(9) } +sub _sql_server_2012_or_higher { shift->__sql_server_x_or_higher(11) } + sub _prep_for_execute { my $self = shift; - my ($op, $extra_bind, $ident, $args) = @_; + my ($op, $ident, $args) = @_; # 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]); + + my $colinfo = $ident->columns_info([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 ( + $colinfo->{$col}{data_type} + && + $colinfo->{$col}{data_type} =~ /^money\z/i + ) { my $val = $fields->{$col}; $fields->{$col} = \['CAST(? AS MONEY)', [ $col => $val ]]; } @@ -56,17 +68,16 @@ sub _prep_for_execute { my ($sql, $bind) = $self->next::method (@_); - if ($op eq 'insert') { - $sql .= ';SELECT SCOPE_IDENTITY()'; - - my $col_info = $self->_resolve_column_info($ident, [map $_->[0], @{$bind}]); - if (List::Util::first { $_->{is_auto_increment} } (values %$col_info) ) { - - my $table = $ident->from; - my $identity_insert_on = "SET IDENTITY_INSERT $table ON"; - my $identity_insert_off = "SET IDENTITY_INSERT $table OFF"; - $sql = "$identity_insert_on; $sql; $identity_insert_off"; - } + # 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); @@ -74,38 +85,166 @@ sub _prep_for_execute { sub _execute { my $self = shift; - my ($op) = @_; - my ($rv, $sth, @bind) = $self->dbh_do($self->can('_dbh_execute'), @_); - if ($op eq 'insert') { - $self->{_scope_identity} = $sth->fetchrow_array; - $sth->finish; + # 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) = 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 } -sub last_insert_id { shift->{_scope_identity} } - -sub build_datetime_parser { +# +# 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 $type = "DateTime::Format::Strptime"; - eval "use ${type}"; - $self->throw_exception("Couldn't load ${type}: $@") if $@; - return $type->new( pattern => '%Y-%m-%d %H:%M:%S' ); # %F %T + 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_maker_opts { - my ( $self, $opts ) = @_; +sub sql_limit_dialect { + my $self = shift; - if ( $opts ) { - $self->{_sql_maker_opts} = { %$opts }; + my $supports_rno = $self->_sql_server_2005_or_higher; + + unless (defined $supports_rno) { + # 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. + try { + $self->_get_dbh->selectrow_array('SELECT row_number() OVER (ORDER BY rand())'); + $supports_rno = 1; + }; } - return { limit_dialect => 'Top', %{$self->{_sql_maker_opts}||{}} }; + return $supports_rno ? 'RowNumberOver' : 'Top'; +} + +sub _ping { + my $self = shift; + + my $dbh = $self->_dbh or return 0; + + local $dbh->{RaiseError} = 1; + local $dbh->{PrintError} = 0; + + return try { + $dbh->do('select 1'); + 1; + } catch { + 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; @@ -123,6 +262,8 @@ 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 @@ -131,9 +272,77 @@ 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