fix race condition in last_insert_id with placeholders
[dbsrgits/DBIx-Class.git] / lib / DBIx / Class / Storage / DBI / Sybase.pm
1 package DBIx::Class::Storage::DBI::Sybase;
2
3 use strict;
4 use warnings;
5
6 use base qw/
7     DBIx::Class::Storage::DBI::Sybase::Base
8     DBIx::Class::Storage::DBI
9 /;
10 use mro 'c3';
11 use Carp::Clan qw/^DBIx::Class/;
12 use List::Util ();
13
14 __PACKAGE__->mk_group_accessors('simple' =>
15     qw/_identity _blob_log_on_update/
16 );
17
18 =head1 NAME
19
20 DBIx::Class::Storage::DBI::Sybase - Sybase support for DBIx::Class
21
22 =head1 SYNOPSIS
23
24 This subclass supports L<DBD::Sybase> for real Sybase databases.  If you are
25 using an MSSQL database via L<DBD::Sybase>, your storage will be reblessed to
26 L<DBIx::Class::Storage::DBI::Sybase::Microsoft_SQL_Server>.
27
28 =head1 DESCRIPTION
29
30 If your version of Sybase does not support placeholders, then your storage
31 will be reblessed to L<DBIx::Class::Storage::DBI::Sybase::NoBindVars>. You can
32 also enable that driver explicitly, see the documentation for more details.
33
34 With this driver there is unfortunately no way to get the C<last_insert_id>
35 without doing a C<select max(col)>.
36
37 But your queries will be cached.
38
39 You need a version of L<DBD::Sybase> compiled with the Sybase OpenClient
40 libraries, B<NOT> FreeTDS, for placeholder support. Otherwise your storage will
41 be automatically reblessed into C<::NoBindVars>.
42
43 A recommended L<DBIx::Class::Storage::DBI/connect_info> setting:
44
45   on_connect_call => [['datetime_setup'], ['blob_setup', log_on_update => 0]]
46
47 =head1 METHODS
48
49 =cut
50
51 sub _rebless {
52   my $self = shift;
53
54   if (ref($self) eq 'DBIx::Class::Storage::DBI::Sybase') {
55     my $dbtype = eval {
56       @{$self->dbh->selectrow_arrayref(qq{sp_server_info \@attribute_id=1})}[2]
57     } || '';
58
59     my $exception = $@;
60     $dbtype =~ s/\W/_/gi;
61     my $subclass = "DBIx::Class::Storage::DBI::Sybase::${dbtype}";
62
63     if (!$exception && $dbtype && $self->load_optional_class($subclass)) {
64       bless $self, $subclass;
65       $self->_rebless;
66     } else { # real Sybase
67       my $no_bind_vars = 'DBIx::Class::Storage::DBI::Sybase::NoBindVars';
68
69       if ($self->_using_freetds) {
70         carp <<'EOF';
71
72 Your version of Sybase potentially supports placeholders and query caching,
73 however you seem to be using FreeTDS which does not (yet?) support this.
74
75 Please recompile DBD::Sybase with the Sybase OpenClient libraries if you want
76 these features.
77
78 TEXT/IMAGE column support will also not work under FreeTDS.
79
80 See perldoc DBIx::Class::Storage::DBI::Sybase for more details.
81 EOF
82         $self->ensure_class_loaded($no_bind_vars);
83         bless $self, $no_bind_vars;
84         $self->_rebless;
85       }
86
87       if (not $self->dbh->{syb_dynamic_supported}) {
88         $self->ensure_class_loaded($no_bind_vars);
89         bless $self, $no_bind_vars;
90         $self->_rebless;
91       }
92  
93       $self->_set_maxConnect;
94     }
95   }
96 }
97
98 sub _using_freetds {
99   my $self = shift;
100
101   return $self->_dbh->{syb_oc_version} =~ /freetds/i;
102 }
103
104 sub _set_maxConnect {
105   my $self = shift;
106
107   my $dsn = $self->_dbi_connect_info->[0];
108
109   return if ref($dsn) eq 'CODE';
110
111   if ($dsn !~ /maxConnect=/) {
112     $self->_dbi_connect_info->[0] = "$dsn;maxConnect=256";
113     my $connected = defined $self->_dbh;
114     $self->disconnect;
115     $self->ensure_connected if $connected;
116   }
117 }
118
119 =head2 connect_call_blob_setup
120
121 Used as:
122
123   on_connect_call => [ [ 'blob_setup', log_on_update => 0 ] ]
124
125 Does C<< $dbh->{syb_binary_images} = 1; >> to return C<IMAGE> data as raw binary
126 instead of as a hex string.
127
128 Recommended.
129
130 Also sets the C<log_on_update> value for blob write operations. The default is
131 C<1>, but C<0> is better if your database is configured for it.
132
133 See
134 L<DBD::Sybase/Handling_IMAGE/TEXT_data_with_syb_ct_get_data()/syb_ct_send_data()>.
135
136 =cut
137
138 sub connect_call_blob_setup {
139   my $self = shift;
140   my %args = @_;
141   my $dbh = $self->_dbh;
142   $dbh->{syb_binary_images} = 1;
143
144   $self->_blob_log_on_update($args{log_on_update})
145     if exists $args{log_on_update};
146 }
147
148 sub _is_lob_type {
149   my $self = shift;
150   my $type = shift;
151   $type && $type =~ /(?:text|image|lob|bytea|binary|memo)/i;
152 }
153
154 ## This will be useful if we ever implement BLOB filehandle inflation and will
155 ## need to use the API, but for now it isn't.
156 #
157 #sub order_columns_for_select {
158 #  my ($self, $source, $columns) = @_;
159 #
160 #  my (@non_blobs, @blobs);
161 #
162 #  for my $col (@$columns) {
163 #    if ($self->_is_lob_type($source->column_info($col)->{data_type})) {
164 #      push @blobs, $col;
165 #    } else {
166 #      push @non_blobs, $col;
167 #    }
168 #  }
169 #
170 #  croak "cannot select more than a one TEXT/IMAGE column at a time"
171 #    if @blobs > 1;
172 #
173 #  return (@non_blobs, @blobs);
174 #}
175
176 # the select-piggybacking-on-insert trick stolen from odbc/mssql
177 sub _prep_for_execute {
178   my $self = shift;
179   my ($op, $extra_bind, $ident, $args) = @_;
180
181   my ($sql, $bind) = $self->next::method (@_);
182
183   if ($op eq 'insert') {
184     my ($identity_insert_on, $identity_insert_off, $identity_col);
185     my $table = $ident->from;
186
187     my $bind_info = $self->_resolve_column_info($ident, [map $_->[0], @{$bind}]);
188     $identity_col =
189 List::Util::first { $bind_info->{$_}{is_auto_increment} } (keys %$bind_info);
190
191     if ($identity_col) {
192       $identity_insert_on  = "SET IDENTITY_INSERT $table ON";
193       $identity_insert_off = "SET IDENTITY_INSERT $table OFF";
194     } else {
195       $identity_col = List::Util::first {
196         $ident->column_info($_)->{is_auto_increment}
197       } $ident->columns;
198     }
199
200     if ($identity_col) {
201 # Sybase has nested transactions, only the outermost is actually committed
202       $sql =
203         "BEGIN TRANSACTION\n" .
204         ($identity_insert_on  ? "$identity_insert_on\n"  : '') .
205         "$sql\n" .
206         ($identity_insert_off ? "$identity_insert_off\n" : '') .
207         $self->_fetch_identity_sql($ident, $identity_col) . "\n" .
208         "COMMIT";
209     }
210   }
211
212   return ($sql, $bind);
213 }
214
215 sub _fetch_identity_sql {
216   my ($self, $source, $col) = @_;
217
218   return "SELECT MAX($col) FROM ".$source->from;
219 }
220
221 sub _execute {
222   my $self = shift;
223   my ($op) = @_;
224
225   my ($rv, $sth, @bind) = $self->dbh_do($self->can('_dbh_execute'), @_);
226
227   if ($op eq 'insert') {
228     $self->_identity($sth->fetchrow_array);
229     $sth->finish;
230   }
231
232   return wantarray ? ($rv, $sth, @bind) : $rv;
233 }
234
235 sub last_insert_id { shift->_identity }
236
237 # override to handle TEXT/IMAGE
238 sub insert {
239   my ($self, $source, $to_insert) = splice @_, 0, 3;
240   my $dbh = $self->_dbh;
241
242   my $blob_cols = $self->_remove_blob_cols($source, $to_insert);
243
244   my $updated_cols = $self->next::method($source, $to_insert, @_);
245
246   $self->_insert_blobs($source, $blob_cols, $to_insert) if %$blob_cols;
247
248   return $updated_cols;
249 }
250
251 sub update {
252   my ($self, $source)  = splice @_, 0, 2;
253   my ($fields, $where) = @_;
254   my $wantarray        = wantarray;
255
256   my $blob_cols = $self->_remove_blob_cols($source, $fields);
257
258   my @res;
259   if ($wantarray) {
260     @res    = $self->next::method($source, @_);
261   } else {
262     $res[0] = $self->next::method($source, @_);
263   }
264
265   $self->_update_blobs($source, $blob_cols, $where) if %$blob_cols;
266
267   return $wantarray ? @res : $res[0];
268 }
269
270 sub _remove_blob_cols {
271   my ($self, $source, $fields) = @_;
272
273   my %blob_cols;
274
275   for my $col (keys %$fields) {
276     if ($self->_is_lob_type($source->column_info($col)->{data_type})) {
277       $blob_cols{$col} = delete $fields->{$col};
278       $fields->{$col} = \"''";
279     }
280   }
281
282   return \%blob_cols;
283 }
284
285 sub _update_blobs {
286   my ($self, $source, $blob_cols, $where) = @_;
287
288   my (@primary_cols) = $source->primary_columns;
289
290   croak "Cannot update TEXT/IMAGE column(s) without a primary key"
291     unless @primary_cols;
292
293 # check if we're updating a single row by PK
294   my $pk_cols_in_where = 0;
295   for my $col (@primary_cols) {
296     $pk_cols_in_where++ if defined $where->{$col};
297   }
298   my @rows;
299
300   if ($pk_cols_in_where == @primary_cols) {
301     my %row_to_update;
302     @row_to_update{@primary_cols} = @{$where}{@primary_cols};
303     @rows = \%row_to_update;
304   } else {
305     my $rs = $source->resultset->search(
306       $where,
307       {
308         result_class => 'DBIx::Class::ResultClass::HashRefInflator',
309         select => \@primary_cols
310       }
311     );
312     @rows = $rs->all; # statement must finish
313   }
314
315   for my $row (@rows) {
316     $self->_insert_blobs($source, $blob_cols, $row);
317   }
318 }
319
320 sub _insert_blobs {
321   my ($self, $source, $blob_cols, $row) = @_;
322   my $dbh = $self->dbh;
323
324   my $table = $source->from;
325
326   my %row = %$row;
327   my (@primary_cols) = $source->primary_columns;
328
329   croak "Cannot update TEXT/IMAGE column(s) without a primary key"
330     unless @primary_cols;
331
332   if ((grep { defined $row{$_} } @primary_cols) != @primary_cols) {
333     if (@primary_cols == 1) {
334       my $col = $primary_cols[0];
335       $row{$col} = $self->last_insert_id($source, $col);
336     } else {
337       croak "Cannot update TEXT/IMAGE column(s) without primary key values";
338     }
339   }
340
341   for my $col (keys %$blob_cols) {
342     my $blob = $blob_cols->{$col};
343     my $sth;
344
345     if (not $self->isa('DBIx::Class::Storage::DBI::NoBindVars')) {
346       my $search_cond = join ',' => map "$_ = ?", @primary_cols;
347
348       $sth = $self->sth(
349         "select $col from $table where $search_cond"
350       );
351       $sth->execute(map $row{$_}, @primary_cols);
352     } else {
353       my $search_cond = join ',' => map "$_ = $row{$_}", @primary_cols;
354
355       $sth = $dbh->prepare(
356         "select $col from $table where $search_cond"
357       );
358       $sth->execute;
359     }
360
361     eval {
362       while ($sth->fetch) {
363         $sth->func('CS_GET', 1, 'ct_data_info') or die $sth->errstr;
364       }
365       $sth->func('ct_prepare_send') or die $sth->errstr;
366
367       my $log_on_update = $self->_blob_log_on_update;
368       $log_on_update    = 1 if not defined $log_on_update;
369
370       $sth->func('CS_SET', 1, {
371         total_txtlen => length($blob),
372         log_on_update => $log_on_update
373       }, 'ct_data_info') or die $sth->errstr;
374
375       $sth->func($blob, length($blob), 'ct_send_data') or die $sth->errstr;
376
377       $sth->func('ct_finish_send') or die $sth->errstr;
378     };
379     my $exception = $@;
380     $sth->finish;
381     croak $exception if $exception;
382   }
383 }
384
385 =head2 connect_call_datetime_setup
386
387 Used as:
388
389   on_connect_call => 'datetime_setup'
390
391 In L<DBIx::Class::Storage::DBI/connect_info> to set:
392
393   $dbh->syb_date_fmt('ISO_strict'); # output fmt: 2004-08-21T14:36:48.080Z
394   $dbh->do('set dateformat mdy');   # input fmt:  08/13/1979 18:08:55.080
395
396 On connection for use with L<DBIx::Class::InflateColumn::DateTime>, using
397 L<DateTime::Format::Sybase>, which you will need to install.
398
399 This works for both C<DATETIME> and C<SMALLDATETIME> columns, although
400 C<SMALLDATETIME> columns only have minute precision.
401
402 =cut
403
404 {
405   my $old_dbd_warned = 0;
406
407   sub connect_call_datetime_setup {
408     my $self = shift;
409     my $dbh = $self->_dbh;
410
411     if ($dbh->can('syb_date_fmt')) {
412       $dbh->syb_date_fmt('ISO_strict');
413     } elsif (not $old_dbd_warned) {
414       carp "Your DBD::Sybase is too old to support ".
415       "DBIx::Class::InflateColumn::DateTime, please upgrade!";
416       $old_dbd_warned = 1;
417     }
418
419     $dbh->do('set dateformat mdy');
420
421     1;
422   }
423 }
424
425 sub datetime_parser_type { "DateTime::Format::Sybase" }
426
427 # savepoint support using ASE syntax
428
429 sub _svp_begin {
430   my ($self, $name) = @_;
431
432   $self->dbh->do("SAVE TRANSACTION $name");
433 }
434
435 # A new SAVE TRANSACTION with the same name releases the previous one.
436 sub _svp_release { 1 }
437
438 sub _svp_rollback {
439   my ($self, $name) = @_;
440
441   $self->dbh->do("ROLLBACK TRANSACTION $name");
442 }
443
444 1;
445
446 =head1 MAXIMUM CONNECTIONS
447
448 L<DBD::Sybase> makes separate connections to the server for active statements in
449 the background. By default the number of such connections is limited to 25, on
450 both the client side and the server side.
451
452 This is a bit too low, so on connection the clientside setting is set to C<256>
453 (see L<DBD::Sybase/maxConnect>.) You can override it to whatever setting you
454 like in the DSN.
455
456 See
457 L<http://infocenter.sybase.com/help/index.jsp?topic=/com.sybase.help.ase_15.0.sag1/html/sag1/sag1272.htm>
458 for information on changing the setting on the server side.
459
460 =head1 DATES
461
462 See L</connect_call_datetime_setup> to setup date formats
463 for L<DBIx::Class::InflateColumn::DateTime>.
464
465 =head1 IMAGE AND TEXT COLUMNS
466
467 L<DBD::Sybase> compiled with FreeTDS will B<NOT> work with C<TEXT/IMAGE>
468 columns.
469
470 See L</connect_call_blob_setup> for a L<DBIx::Class::Storage::DBI/connect_info>
471 setting you need to work with C<IMAGE> columns.
472
473 =head1 AUTHORS
474
475 See L<DBIx::Class/CONTRIBUTORS>.
476
477 =head1 LICENSE
478
479 You may distribute this code under the same terms as Perl itself.
480
481 =cut
482 # vim:sts=2 sw=2: