X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FDBIx%2FClass%2FStorage%2FDBI%2FSybase.pm;h=35608b00d34e027ec83a2b99b586de2d287c5e63;hb=c8636ca6f869b2e9c76a166440bbb341ab44420c;hp=c12c124f588b836d6e7787f0fda4fbeed94ba5a2;hpb=65bab8da1f45eba91d9d3817c923633d1126e756;p=dbsrgits%2FDBIx-Class.git diff --git a/lib/DBIx/Class/Storage/DBI/Sybase.pm b/lib/DBIx/Class/Storage/DBI/Sybase.pm index c12c124..35608b0 100644 --- a/lib/DBIx/Class/Storage/DBI/Sybase.pm +++ b/lib/DBIx/Class/Storage/DBI/Sybase.pm @@ -5,14 +5,18 @@ use warnings; use base qw/ DBIx::Class::Storage::DBI::Sybase::Base - DBIx::Class::Storage::DBI /; use mro 'c3'; use Carp::Clan qw/^DBIx::Class/; +use List::Util (); + +__PACKAGE__->mk_group_accessors('simple' => + qw/_identity _blob_log_on_update auto_cast _insert_txn/ +); =head1 NAME -DBIx::Class::Storage::DBI::Sybase - Storage::DBI subclass for Sybase +DBIx::Class::Storage::DBI::Sybase - Sybase support for DBIx::Class =head1 SYNOPSIS @@ -27,26 +31,18 @@ will be reblessed to L. You can also enable that driver explicitly, see the documentation for more details. With this driver there is unfortunately no way to get the C -without doing a C. But your queries will be cached. -You need a version of L compiled with the Sybase OpenClient -libraries, B FreeTDS, for placeholder support. Otherwise your storage will -be automatically reblessed into C<::NoBindVars>. - -A recommended L settings: +A recommended L setting: - on_connect_call => [['datetime_setup'], [blob_setup => log_on_update => 0]] + on_connect_call => [['datetime_setup'], ['blob_setup', log_on_update => 0]] =head1 METHODS =cut -__PACKAGE__->mk_group_accessors('simple' => - qw/_blob_log_on_update/ -); - sub _rebless { my $self = shift; @@ -65,64 +61,69 @@ sub _rebless { } else { # real Sybase my $no_bind_vars = 'DBIx::Class::Storage::DBI::Sybase::NoBindVars'; - if (not $self->dbh->{syb_dynamic_supported}) { - $self->ensure_class_loaded($no_bind_vars); - bless $self, $no_bind_vars; - $self->_rebless; - } - - if ($self->_using_freetds) { - carp <<'EOF'; +# This is reset to 0 in ::NoBindVars, only necessary because we use max(col) to +# get the identity. + $self->_insert_txn(1); + + if ($self->using_freetds) { + carp <<'EOF' unless $ENV{DBIC_SYBASE_FREETDS_NOWARN}; -Your version of Sybase potentially supports placeholders and query caching, -however you seem to be using FreeTDS which does not (yet?) support this. +You are using FreeTDS with Sybase. -Please recompile DBD::Sybase with the Sybase OpenClient libraries if you want -these features. +We will do our best to support this configuration, but please consider this +support experimental. -TEXT/IMAGE column support will also not work under FreeTDS. +TEXT/IMAGE columns will definitely not work. + +You are encouraged to recompile DBD::Sybase with the Sybase Open Client libraries +instead. See perldoc DBIx::Class::Storage::DBI::Sybase for more details. + +To turn off this warning set the DBIC_SYBASE_FREETDS_NOWARN environment +variable. EOF + if (not $self->placeholders_with_type_conversion_supported) { + if ($self->placeholders_supported) { + $self->auto_cast(1); + } else { + $self->ensure_class_loaded($no_bind_vars); + bless $self, $no_bind_vars; + $self->_rebless; + } + } + + $self->set_textsize; # based on LongReadLen in connect_info + + } elsif (not $self->dbh->{syb_dynamic_supported}) { +# not necessarily FreeTDS, but no placeholders nevertheless $self->ensure_class_loaded($no_bind_vars); bless $self, $no_bind_vars; $self->_rebless; } - $self->_set_maxConnect; + + $self->_set_max_connect(256); } } } -{ - my $using_freetds = undef; - - sub _using_freetds { - my $self = shift; - my $dbh = $self->_dbh; - - return $using_freetds if defined $using_freetds; - -# local $dbh->{syb_rowcount} = 1; # this is broken in freetds -# $using_freetds = @{ $dbh->selectall_arrayref('sp_help') } != 1; - - $using_freetds = $dbh->{syb_oc_version} =~ /freetds/i; - - return $using_freetds; - } -} - -sub _set_maxConnect { +# Make sure we have CHAINED mode turned on if AutoCommit is off in non-FreeTDS +# DBD::Sybase (since we don't know how DBD::Sybase was compiled.) If however +# we're using FreeTDS, CHAINED mode turns on an implicit transaction which we +# only want when AutoCommit is off. +sub _populate_dbh { my $self = shift; - my $dsn = $self->_dbi_connect_info->[0]; + $self->next::method(@_); - return if ref($dsn) eq 'CODE'; - - if ($dsn !~ /maxConnect=/) { - $self->_dbi_connect_info->[0] = "$dsn;maxConnect=256"; - my $connected = defined $self->_dbh; - $self->disconnect; - $self->ensure_connected if $connected; + if (not $self->using_freetds) { + $self->_dbh->{syb_chained_txn} = 1; + } else { + if ($self->_dbh_autocommit) { + $self->_dbh->do('SET CHAINED OFF'); + } else { + $self->_dbh->do('SET CHAINED ON'); + } } } @@ -130,7 +131,7 @@ sub _set_maxConnect { Used as: - on_connect_call => [ [ blob_setup => log_on_update => 0 ] ] + on_connect_call => [ [ 'blob_setup', log_on_update => 0 ] ] Does C<< $dbh->{syb_binary_images} = 1; >> to return C data as raw binary instead of as a hex string. @@ -155,41 +156,171 @@ sub connect_call_blob_setup { if exists $args{log_on_update}; } +=head2 connect_call_set_auto_cast + +In some configurations (usually with L) statements with values bound +to columns or conditions that are not strings will throw implicit type +conversion errors. For L this is automatically detected, and this +option is set. + +It converts placeholders to: + + CAST(? as $type) + +the type is taken from the L setting from +your Result class, and mapped to a Sybase type using a mapping based on +L if necessary. + +This setting can also be set outside of +L at any time using: + + $schema->storage->auto_cast(1); + +=cut + +sub connect_call_set_auto_cast { + my $self = shift; + $self->auto_cast(1); +} + sub _is_lob_type { my $self = shift; my $type = shift; $type && $type =~ /(?:text|image|lob|bytea|binary|memo)/i; } -## This will be useful if we ever implement BLOB filehandle inflation and will -## need to use the API, but for now it isn't. -# -#sub order_columns_for_select { -# my ($self, $source) = @_; -# -# my (@non_blobs, @blobs); -# -# for my $col ($source->columns) { -# if ($self->_is_lob_type($source->column_info($col)->{data_type})) { -# push @blobs, $col; -# } else { -# push @non_blobs, $col; -# } -# } -# -# croak "cannot select more than a one TEXT/IMAGE column at a time" -# if @blobs > 1; +# The select-piggybacking-on-insert trick stolen from odbc/mssql +sub _prep_for_execute { + my $self = shift; + my ($op, $extra_bind, $ident, $args) = @_; + + my ($sql, $bind) = $self->next::method (@_); + +# Some combinations of FreeTDS and Sybase throw implicit conversion errors for +# all placeeholders, so we convert them into CASTs here. +# Based on code in ::DBI::NoBindVars . # -# return (@non_blobs, @blobs); -#} +# If we're using ::NoBindVars, there are no binds by this point so this code +# gets skippeed. + if ($self->auto_cast && @$bind) { + my $new_sql; + my @sql_part = split /\?/, $sql; + my $col_info = $self->_resolve_column_info($ident,[ map $_->[0], @$bind ]); + + foreach my $bound (@$bind) { + my $col = $bound->[0]; + my $syb_type = $self->_syb_base_type($col_info->{$col}{data_type}); + + foreach my $data (@{$bound}[1..$#$bound]) { + $new_sql .= shift(@sql_part) . + ($syb_type ? "CAST(? AS $syb_type)" : '?'); + } + } + $new_sql .= join '', @sql_part; + $sql = $new_sql; + } + + if ($op eq 'insert') { + my $table = $ident->from; + + my $bind_info = $self->_resolve_column_info( + $ident, [map $_->[0], @{$bind}] + ); + my $identity_col = +List::Util::first { $bind_info->{$_}{is_auto_increment} } (keys %$bind_info); + + if ($identity_col) { + $sql = +"SET IDENTITY_INSERT $table ON\n" . +"$sql\n" . +"SET IDENTITY_INSERT $table OFF" + } else { + $identity_col = List::Util::first { + $ident->column_info($_)->{is_auto_increment} + } $ident->columns; + } + + if ($identity_col) { + $sql = + "$sql\n" . + $self->_fetch_identity_sql($ident, $identity_col); + } + } + + return ($sql, $bind); +} + +# Stolen from SQLT, with some modifications. This will likely change when the +# SQLT Sybase stuff is redone/fixed-up. +my %TYPE_MAPPING = ( + number => 'numeric', + money => 'money', + varchar => 'varchar', + varchar2 => 'varchar', + timestamp => 'datetime', + text => 'varchar', + real => 'double precision', + comment => 'text', + bit => 'bit', + tinyint => 'smallint', + float => 'double precision', + serial => 'numeric', + bigserial => 'numeric', + boolean => 'varchar', + long => 'varchar', +); + +sub _syb_base_type { + my ($self, $type) = @_; + + $type = lc $type; + $type =~ s/ identity//; -# override to handle TEXT/IMAGE + return uc($TYPE_MAPPING{$type} || $type); +} + +sub _fetch_identity_sql { + my ($self, $source, $col) = @_; + + return "SELECT MAX($col) FROM ".$source->from; +} + +sub _execute { + my $self = shift; + my ($op) = @_; + + my ($rv, $sth, @bind) = $self->dbh_do($self->can('_dbh_execute'), @_); + + if ($op eq 'insert') { + $self->_identity($sth->fetchrow_array); + $sth->finish; + } + + return wantarray ? ($rv, $sth, @bind) : $rv; +} + +sub last_insert_id { shift->_identity } + +# override to handle TEXT/IMAGE and to do a transaction if necessary sub insert { my ($self, $source, $to_insert) = splice @_, 0, 3; + my $dbh = $self->_dbh; my $blob_cols = $self->_remove_blob_cols($source, $to_insert); - my $updated_cols = $self->next::method($source, $to_insert, @_); +# We have to do the insert in a transaction to avoid race conditions with the +# SELECT MAX(COL) identity method used when placeholders are enabled. + my $updated_cols = do { + if ($self->_insert_txn && (not $self->{transaction_depth})) { + my $args = \@_; + my $method = $self->next::can; + $self->txn_do( + sub { $self->$method($source, $to_insert, @$args) } + ); + } else { + $self->next::method($source, $to_insert, @_); + } + }; $self->_insert_blobs($source, $blob_cols, $to_insert) if %$blob_cols; @@ -290,26 +421,18 @@ sub _insert_blobs { my $blob = $blob_cols->{$col}; my $sth; - if (not $self->isa('DBIx::Class::Storage::DBI::NoBindVars')) { - my $search_cond = join ',' => map "$_ = ?", @primary_cols; - - $sth = $self->sth( - "select $col from $table where $search_cond" - ); - $sth->execute(map $row{$_}, @primary_cols); - } else { - my $search_cond = join ',' => map "$_ = $row{$_}", @primary_cols; - - $sth = $dbh->prepare( - "select $col from $table where $search_cond" - ); - $sth->execute; - } + my %where = map { ($_, $row{$_}) } @primary_cols; + my $cursor = $source->resultset->search(\%where, { + select => [$col] + })->cursor; + $cursor->next; + $sth = $cursor->sth; eval { - while ($sth->fetch) { + do { $sth->func('CS_GET', 1, 'ct_data_info') or die $sth->errstr; - } + } while $sth->fetch; + $sth->func('ct_prepare_send') or die $sth->errstr; my $log_on_update = $self->_blob_log_on_update; @@ -325,8 +448,16 @@ sub _insert_blobs { $sth->func('ct_finish_send') or die $sth->errstr; }; my $exception = $@; - $sth->finish; - croak $exception if $exception; + $sth->finish if $sth; + if ($exception) { + if ($self->using_freetds) { + croak +"TEXT/IMAGE operation failed, probably because you're using FreeTDS: " . +$exception; + } else { + croak $exception; + } + } } } @@ -357,6 +488,7 @@ C columns only have minute precision. my $dbh = $self->_dbh; if ($dbh->can('syb_date_fmt')) { +# amazingly, this works with FreeTDS $dbh->syb_date_fmt('ISO_strict'); } elsif (not $old_dbd_warned) { carp "Your DBD::Sybase is too old to support ". @@ -364,7 +496,7 @@ C columns only have minute precision. $old_dbd_warned = 1; } - $dbh->do('set dateformat mdy'); + $dbh->do('SET DATEFORMAT mdy'); 1; } @@ -372,28 +504,96 @@ C columns only have minute precision. sub datetime_parser_type { "DateTime::Format::Sybase" } -sub _dbh_last_insert_id { - my ($self, $dbh, $source, $col) = @_; +# ->begin_work and such have no effect with FreeTDS but we run them anyway to +# let the DBD keep any state it needs to. +# +# If they ever do start working, the extra statements will do no harm (because +# Sybase supports nested transactions.) - # sorry, there's no other way! - my $sth = $self->sth("select max($col) from ".$source->from); - my ($id) = $dbh->selectrow_array($sth); - $sth->finish; +sub _dbh_begin_work { + my $self = shift; + $self->next::method(@_); + if ($self->using_freetds) { + $self->dbh->do('BEGIN TRAN'); + } +} + +sub _dbh_commit { + my $self = shift; + if ($self->using_freetds) { + $self->_dbh->do('COMMIT'); + } + return $self->next::method(@_); +} + +sub _dbh_rollback { + my $self = shift; + if ($self->using_freetds) { + $self->_dbh->do('ROLLBACK'); + } + return $self->next::method(@_); +} + +# savepoint support using ASE syntax + +sub _svp_begin { + my ($self, $name) = @_; + + $self->dbh->do("SAVE TRANSACTION $name"); +} + +# A new SAVE TRANSACTION with the same name releases the previous one. +sub _svp_release { 1 } - return $id; +sub _svp_rollback { + my ($self, $name) = @_; + + $self->dbh->do("ROLLBACK TRANSACTION $name"); } 1; +=head1 FreeTDS + +This driver supports L compiled against FreeTDS +(L) to the best of our ability, however it is +recommended that you recompile L against the Sybase Open Client +libraries. They are a part of the Sybase ASE distribution: + +The Open Client FAQ is here: +L. + +Sybase ASE for Linux (which comes with the Open Client libraries) may be +downloaded here: L. + +To see if you're using FreeTDS check C<< $schema->storage->using_freetds >>, or run: + + perl -MDBI -le 'my $dbh = DBI->connect($dsn, $user, $pass); print $dbh->{syb_oc_version}' + +Some versions of the libraries involved will not support placeholders, in which +case the storage will be reblessed to +L. + +In some configurations, placeholders will work but will throw implicit +conversion errors for anything that's not expecting a string. In such a case, +the C option is automatically set, which you may enable yourself with +L (see the description of that method for more +details.) + +In other configurations, placeholers will work just as they do with the Sybase +Open Client libraries. + +Inserts or updates of TEXT/IMAGE columns will B work with FreeTDS. + =head1 MAXIMUM CONNECTIONS -L makes separate connections to the server for active statements in -the background. By default the number of such connections is limited to 25, on -both the client side and the server side. +The TDS protocol makes separate connections to the server for active statements +in the background. By default the number of such connections is limited to 25, +on both the client side and the server side. -This is a bit too low, so on connection the clientside setting is set to C<256> -(see L.) You can override it to whatever setting you -like in the DSN. +This is a bit too low for a complex L application, so on connection +the client side setting is set to C<256> (see L.) You +can override it to whatever setting you like in the DSN. See L @@ -404,10 +604,24 @@ for information on changing the setting on the server side. See L to setup date formats for L. -=head1 IMAGE AND TEXT COLUMNS +=head1 TEXT/IMAGE COLUMNS + +L compiled with FreeTDS will B allow you to insert or update +C columns. + +Setting C<< $dbh->{LongReadLen} >> will also not work with FreeTDS use either: + + $schema->storage->dbh->do("SET TEXTSIZE $bytes"); + +or + + $schema->storage->set_textsize($bytes); + +instead. -L compiled with FreeTDS will B work with C -columns. +However, the C you pass in +L is used to execute the equivalent +C command on connection. See L for a L setting you need to work with C columns.