package DBIx::Class::Storage::DBI::MSSQL; use strict; use warnings; use base qw/DBIx::Class::Storage::DBI::UniqueIdentifier/; use mro 'c3'; use Try::Tiny; use List::Util 'first'; use namespace::clean; __PACKAGE__->mk_group_accessors(simple => qw/ _identity _identity_method _pre_insert_sql _post_insert_sql /); __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 _set_identity_insert { my ($self, $table) = @_; my $stmt = 'SET IDENTITY_INSERT %s %s'; $table = $self->sql_maker->_quote($table); $self->_pre_insert_sql (sprintf $stmt, $table, 'ON'); $self->_post_insert_sql(sprintf $stmt, $table, 'OFF'); } sub insert_bulk { my $self = shift; my ($source, $cols, $data) = @_; my $is_identity_insert = (first { $_->{is_auto_increment} } values %{ $source->columns_info($cols) } ) ? 1 : 0 ; if ($is_identity_insert) { $self->_set_identity_insert ($source->name); } $self->next::method(@_); } sub insert { my $self = shift; my ($source, $to_insert) = @_; my $supplied_col_info = $self->_resolve_column_info($source, [keys %$to_insert] ); my $is_identity_insert = (first { $_->{is_auto_increment} } values %$supplied_col_info) ? 1 : 0; if ($is_identity_insert) { $self->_set_identity_insert ($source->name); } my $updated_cols = $self->next::method(@_); return $updated_cols; } 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 (@_); if ($op eq 'insert') { if (my $prepend = $self->_pre_insert_sql) { $sql = "${prepend}\n${sql}"; $self->_pre_insert_sql(undef); } if (my $append = $self->_post_insert_sql) { $sql = "${sql}\n${append}"; $self->_post_insert_sql(undef); } $sql .= "\nSELECT SCOPE_IDENTITY()"; } return ($sql, $bind); } sub _execute { my $self = shift; my ($op) = @_; my ($rv, $sth, @bind) = $self->next::method(@_); if ($op eq 'insert') { # this should bring back the result of SELECT SCOPE_IDENTITY() we tacked # on in _prep_for_execute above my ($identity) = try { $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 } # # 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 = shift; my ($sql, $prep_bind, @rest) = $self->next::method (@_); # see if this is an ordered subquery my $attrs = $_[3]; if ( $sql !~ /^ \s* SELECT \s+ TOP \s+ \d+ \s+ /xi && 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}; my $max = $self->sql_maker->__max_int; $sql =~ s/^ \s* SELECT \s/SELECT TOP $max /xi; } return wantarray ? ($sql, $prep_bind, @rest) : \[ "($sql)", @$prep_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. 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; 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; =head1 NAME DBIx::Class::Storage::DBI::MSSQL - Base Class for Microsoft SQL Server support in DBIx::Class =head1 SYNOPSIS 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