Unify the MSSQL and DB2 RNO implementations - they are the same
[dbsrgits/DBIx-Class.git] / lib / DBIx / Class / Storage / DBI / MSSQL.pm
CommitLineData
75d07914 1package DBIx::Class::Storage::DBI::MSSQL;
3885cff6 2
75d07914 3use strict;
4use warnings;
3885cff6 5
48fe9087 6use base qw/DBIx::Class::Storage::DBI::AmbiguousGlob DBIx::Class::Storage::DBI/;
2ad62d97 7use mro 'c3';
3885cff6 8
5a77aa8b 9use List::Util();
10
7b1b2582 11__PACKAGE__->mk_group_accessors(simple => qw/
12 _identity _identity_method
13/);
14
ac93965c 15__PACKAGE__->sql_maker_class('DBIx::Class::SQLAHacks::MSSQL');
16
afcfff01 17sub _set_identity_insert {
18 my ($self, $table) = @_;
64690266 19
20 my $sql = sprintf (
afcfff01 21 'SET IDENTITY_INSERT %s ON',
64690266 22 $self->sql_maker->_quote ($table),
afcfff01 23 );
64690266 24
25 my $dbh = $self->_get_dbh;
26 eval { $dbh->do ($sql) };
27 if ($@) {
28 $self->throw_exception (sprintf "Error executing '%s': %s",
29 $sql,
30 $dbh->errstr,
31 );
32 }
afcfff01 33}
34
aac1a358 35sub _unset_identity_insert {
36 my ($self, $table) = @_;
37
38 my $sql = sprintf (
39 'SET IDENTITY_INSERT %s OFF',
40 $self->sql_maker->_quote ($table),
41 );
42
43 my $dbh = $self->_get_dbh;
44 $dbh->do ($sql);
45}
46
5a77aa8b 47sub insert_bulk {
48 my $self = shift;
49 my ($source, $cols, $data) = @_;
50
aac1a358 51 my $is_identity_insert = (List::Util::first
afcfff01 52 { $source->column_info ($_)->{is_auto_increment} }
53 (@{$cols})
aac1a358 54 )
55 ? 1
56 : 0;
5a77aa8b 57
aac1a358 58 if ($is_identity_insert) {
59 $self->_set_identity_insert ($source->name);
5a77aa8b 60 }
61
62 $self->next::method(@_);
63
aac1a358 64 if ($is_identity_insert) {
65 $self->_unset_identity_insert ($source->name);
5a77aa8b 66 }
67}
68
57ee81d0 69# support MSSQL GUID column types
70
ca791b95 71sub insert {
72 my $self = shift;
73 my ($source, $to_insert) = @_;
74
afcfff01 75 my $supplied_col_info = $self->_resolve_column_info($source, [keys %$to_insert] );
ca791b95 76
77 my %guid_cols;
78 my @pk_cols = $source->primary_columns;
79 my %pk_cols;
80 @pk_cols{@pk_cols} = ();
81
82 my @pk_guids = grep {
be294d66 83 $source->column_info($_)->{data_type}
84 &&
ca791b95 85 $source->column_info($_)->{data_type} =~ /^uniqueidentifier/i
86 } @pk_cols;
87
88 my @auto_guids = grep {
be294d66 89 $source->column_info($_)->{data_type}
90 &&
ca791b95 91 $source->column_info($_)->{data_type} =~ /^uniqueidentifier/i
92 &&
93 $source->column_info($_)->{auto_nextval}
94 } grep { not exists $pk_cols{$_} } $source->columns;
95
96 my @get_guids_for =
97 grep { not exists $to_insert->{$_} } (@pk_guids, @auto_guids);
98
afcfff01 99 my $updated_cols = {};
100
ca791b95 101 for my $guid_col (@get_guids_for) {
9ae966b9 102 my ($new_guid) = $self->_get_dbh->selectrow_array('SELECT NEWID()');
ca791b95 103 $updated_cols->{$guid_col} = $to_insert->{$guid_col} = $new_guid;
104 }
105
aac1a358 106 my $is_identity_insert = (List::Util::first { $_->{is_auto_increment} } (values %$supplied_col_info) )
107 ? 1
108 : 0;
109
110 if ($is_identity_insert) {
111 $self->_set_identity_insert ($source->name);
afcfff01 112 }
113
ca791b95 114 $updated_cols = { %$updated_cols, %{ $self->next::method(@_) } };
115
aac1a358 116 if ($is_identity_insert) {
117 $self->_unset_identity_insert ($source->name);
118 }
119
120
ca791b95 121 return $updated_cols;
122}
123
5a77aa8b 124sub _prep_for_execute {
125 my $self = shift;
126 my ($op, $extra_bind, $ident, $args) = @_;
127
128# cast MONEY values properly
129 if ($op eq 'insert' || $op eq 'update') {
130 my $fields = $args->[0];
5a77aa8b 131
132 for my $col (keys %$fields) {
1537084d 133 # $ident is a result source object with INSERT/UPDATE ops
be294d66 134 if ($ident->column_info ($col)->{data_type}
135 &&
136 $ident->column_info ($col)->{data_type} =~ /^money\z/i) {
5a77aa8b 137 my $val = $fields->{$col};
138 $fields->{$col} = \['CAST(? AS MONEY)', [ $col => $val ]];
139 }
140 }
141 }
142
143 my ($sql, $bind) = $self->next::method (@_);
144
145 if ($op eq 'insert') {
146 $sql .= ';SELECT SCOPE_IDENTITY()';
147
5a77aa8b 148 }
149
150 return ($sql, $bind);
151}
152
153sub _execute {
154 my $self = shift;
155 my ($op) = @_;
156
157 my ($rv, $sth, @bind) = $self->dbh_do($self->can('_dbh_execute'), @_);
1537084d 158
5a77aa8b 159 if ($op eq 'insert') {
5a77aa8b 160
1537084d 161 # this should bring back the result of SELECT SCOPE_IDENTITY() we tacked
162 # on in _prep_for_execute above
4ffa5700 163 my ($identity) = eval { $sth->fetchrow_array };
ed8de058 164
1537084d 165 # SCOPE_IDENTITY failed, but we can do something else
166 if ( (! $identity) && $self->_identity_method) {
167 ($identity) = $self->_dbh->selectrow_array(
168 'select ' . $self->_identity_method
169 );
170 }
7b1b2582 171
1537084d 172 $self->_identity($identity);
173 $sth->finish;
7b1b2582 174 }
175
1537084d 176 return wantarray ? ($rv, $sth, @bind) : $rv;
7b1b2582 177}
5a77aa8b 178
7b1b2582 179sub last_insert_id { shift->_identity }
5a77aa8b 180
f0bd60fc 181#
182# MSSQL is retarded wrt ordered subselects. One needs to add a TOP 100%
183# to *all* subqueries, do it here.
184#
185sub _select_args_to_query {
186 my $self = shift;
187
b9066995 188 # _select_args does some shady action at a distance
189 # see DBI.pm for more info
f0bd60fc 190 my $sql_maker = $self->sql_maker;
b9066995 191 my ($op, $bind, $ident, $bind_attrs, $select, $cond, $order, $rows, $offset);
192 {
193 local $sql_maker->{_dbic_rs_attrs};
194 ($op, $bind, $ident, $bind_attrs, $select, $cond, $order, $rows, $offset) = $self->_select_args(@_);
195 }
f0bd60fc 196
b9066995 197 if (
198 ($rows || $offset)
199 ||
200 not scalar $sql_maker->_order_by_chunks ($order->{order_by})
201 ) {
202 # either limited RS or no ordering, just short circuit
f0bd60fc 203 return $self->next::method (@_);
204 }
205
17555a0c 206 my ($sql, $prep_bind, @rest) = $self->next::method (@_);
f0bd60fc 207 $sql =~ s/^ \s* SELECT \s/SELECT TOP 100 PERCENT /xi;
208
209 return wantarray
17555a0c 210 ? ($sql, $prep_bind, @rest)
211 : \[ "($sql)", @$prep_bind ]
f0bd60fc 212 ;
213}
214
215
4c0f4206 216# savepoint syntax is the same as in Sybase ASE
217
218sub _svp_begin {
219 my ($self, $name) = @_;
220
9ae966b9 221 $self->_get_dbh->do("SAVE TRANSACTION $name");
4c0f4206 222}
223
224# A new SAVE TRANSACTION with the same name releases the previous one.
225sub _svp_release { 1 }
226
227sub _svp_rollback {
228 my ($self, $name) = @_;
229
9ae966b9 230 $self->_get_dbh->do("ROLLBACK TRANSACTION $name");
4c0f4206 231}
232
ed8de058 233sub build_datetime_parser {
234 my $self = shift;
235 my $type = "DateTime::Format::Strptime";
236 eval "use ${type}";
237 $self->throw_exception("Couldn't load ${type}: $@") if $@;
eb0323df 238 return $type->new( pattern => '%Y-%m-%d %H:%M:%S' ); # %F %T
239}
240
241sub sqlt_type { 'SQLServer' }
242
243sub _sql_maker_opts {
5a77aa8b 244 my ( $self, $opts ) = @_;
eb0323df 245
5a77aa8b 246 if ( $opts ) {
247 $self->{_sql_maker_opts} = { %$opts };
248 }
eb0323df 249
6553ac38 250 return { limit_dialect => 'RowNumberOver', %{$self->{_sql_maker_opts}||{}} };
ed8de058 251}
3885cff6 252
75d07914 2531;
3885cff6 254
75d07914 255=head1 NAME
3885cff6 256
5a77aa8b 257DBIx::Class::Storage::DBI::MSSQL - Base Class for Microsoft SQL Server support
258in DBIx::Class
3885cff6 259
75d07914 260=head1 SYNOPSIS
3885cff6 261
5a77aa8b 262This is the base class for Microsoft SQL Server support, used by
263L<DBIx::Class::Storage::DBI::ODBC::Microsoft_SQL_Server> and
264L<DBIx::Class::Storage::DBI::Sybase::Microsoft_SQL_Server>.
eb0323df 265
5a77aa8b 266=head1 IMPLEMENTATION NOTES
eb0323df 267
fd05d10a 268=head2 IDENTITY information
269
5a77aa8b 270Microsoft SQL Server supports three methods of retrieving the IDENTITY
271value for inserted row: IDENT_CURRENT, @@IDENTITY, and SCOPE_IDENTITY().
272SCOPE_IDENTITY is used here because it is the safest. However, it must
273be called is the same execute statement, not just the same connection.
eb0323df 274
5a77aa8b 275So, this implementation appends a SELECT SCOPE_IDENTITY() statement
276onto each INSERT to accommodate that requirement.
eb0323df 277
7b1b2582 278C<SELECT @@IDENTITY> can also be used by issuing:
279
280 $self->_identity_method('@@identity');
281
08cdc412 282it will only be used if SCOPE_IDENTITY() fails.
283
284This is more dangerous, as inserting into a table with an on insert trigger that
285inserts into another table with an identity will give erroneous results on
286recent versions of SQL Server.
7b1b2582 287
c84189e1 288=head2 identity insert
fd05d10a 289
290Be aware that we have tried to make things as simple as possible for our users.
c84189e1 291For MSSQL that means that when a user tries to create a row, while supplying an
292explicit value for an autoincrementing column, we will try to issue the
293appropriate database call to make this possible, namely C<SET IDENTITY_INSERT
294$table_name ON>. Unfortunately this operation in MSSQL requires the
295C<db_ddladmin> privilege, which is normally not included in the standard
296write-permissions.
fd05d10a 297
5a77aa8b 298=head1 AUTHOR
3885cff6 299
5a77aa8b 300See L<DBIx::Class/CONTRIBUTORS>.
3885cff6 301
75d07914 302=head1 LICENSE
3885cff6 303
75d07914 304You may distribute this code under the same terms as Perl itself.
3885cff6 305
75d07914 306=cut