X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FDBIx%2FClass%2FStorage%2FDBI%2FSybase.pm;h=b320242f077d7cdf74d6655c52a1d10b7e293806;hb=f6de71113251acc964444e57bf3d38a23dc09ffd;hp=40aa3693568d4369ea4892aa670ba981386dcbab;hpb=98259fe400da233fd4d9260e2b1cb6d9a074c460;p=dbsrgits%2FDBIx-Class.git diff --git a/lib/DBIx/Class/Storage/DBI/Sybase.pm b/lib/DBIx/Class/Storage/DBI/Sybase.pm index 40aa369..b320242 100644 --- a/lib/DBIx/Class/Storage/DBI/Sybase.pm +++ b/lib/DBIx/Class/Storage/DBI/Sybase.pm @@ -3,14 +3,21 @@ package DBIx::Class::Storage::DBI::Sybase; use strict; use warnings; -use Class::C3; -use base qw/DBIx::Class::Storage::DBI/; - +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/ +); =head1 NAME -DBIx::Class::Storage::DBI::Sybase - Storage::DBI subclass for Sybase +DBIx::Class::Storage::DBI::Sybase - Sybase support for DBIx::Class =head1 SYNOPSIS @@ -25,13 +32,17 @@ 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 setting: - on_connect_call => [qw/datetime_setup blob_setup/] + on_connect_call => [['datetime_setup'], ['blob_setup', log_on_update => 0]] =head1 METHODS @@ -52,43 +63,337 @@ sub _rebless { if (!$exception && $dbtype && $self->load_optional_class($subclass)) { bless $self, $subclass; $self->_rebless; - } else { - # real Sybase + } else { # real Sybase + my $no_bind_vars = 'DBIx::Class::Storage::DBI::Sybase::NoBindVars'; + + if ($self->_using_freetds) { + carp <<'EOF'; + +Your version of Sybase potentially supports placeholders and query caching, +however you seem to be using FreeTDS which does not (yet?) support this. + +Please recompile DBD::Sybase with the Sybase OpenClient libraries if you want +these features. + +TEXT/IMAGE column support will also not work under FreeTDS. + +See perldoc DBIx::Class::Storage::DBI::Sybase for more details. +EOF + $self->ensure_class_loaded($no_bind_vars); + bless $self, $no_bind_vars; + $self->_rebless; + } + if (not $self->dbh->{syb_dynamic_supported}) { - bless $self, 'DBIx::Class::Storage:DBI::Sybase::NoBindVars'; + $self->ensure_class_loaded($no_bind_vars); + bless $self, $no_bind_vars; $self->_rebless; } - $self->connect_call_datetime_setup; - $self->connect_call_blob_setup; + + $self->_set_maxConnect; } } } +# Make sure we have CHAINED mode turned on, we don't know how DBD::Sybase was +# compiled. sub _populate_dbh { my $self = shift; $self->next::method(@_); - $self->connect_call_datetime_setup; - $self->connect_call_blob_setup; - 1; + $self->_dbh->{syb_chained_txn} = 1; +} + +sub _using_freetds { + my $self = shift; + + return $self->_dbh->{syb_oc_version} =~ /freetds/i; +} + +sub _set_maxConnect { + my $self = shift; + + my $dsn = $self->_dbi_connect_info->[0]; + + 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; + } } =head2 connect_call_blob_setup Used as: - on_connect_call => 'blob_setup' + 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. Recommended. +Also sets the C value for blob write operations. The default is +C<1>, but C<0> is better if your database is configured for it. + +See +L. + =cut sub connect_call_blob_setup { my $self = shift; + my %args = @_; my $dbh = $self->_dbh; $dbh->{syb_binary_images} = 1; + + $self->_blob_log_on_update($args{log_on_update}) + if exists $args{log_on_update}; +} + +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, $columns) = @_; +# +# my (@non_blobs, @blobs); +# +# for my $col (@$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; +# +# return (@non_blobs, @blobs); +#} + +# 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 (@_); + + 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) . "\n"; + } + } + + return ($sql, $bind); +} + +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 nested txn +sub insert { + my ($self, $source, $to_insert) = splice @_, 0, 3; + my $dbh = $self->_dbh; + + my $blob_cols = $self->_remove_blob_cols($source, $to_insert); + +# Sybase has nested transactions fortunately, because 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 { + local $self->{auto_savepoint} = 1; + my $args = \@_; + my $method = $self->next::can; + $self->txn_do( + sub { $self->$method($source, $to_insert, @$args) } + ); + }; + + $self->_insert_blobs($source, $blob_cols, $to_insert) if %$blob_cols; + + return $updated_cols; +} + +sub update { + my ($self, $source) = splice @_, 0, 2; + my ($fields, $where) = @_; + my $wantarray = wantarray; + + my $blob_cols = $self->_remove_blob_cols($source, $fields); + + my @res; + if ($wantarray) { + @res = $self->next::method($source, @_); + } else { + $res[0] = $self->next::method($source, @_); + } + + $self->_update_blobs($source, $blob_cols, $where) if %$blob_cols; + + return $wantarray ? @res : $res[0]; +} + +sub _remove_blob_cols { + my ($self, $source, $fields) = @_; + + my %blob_cols; + + for my $col (keys %$fields) { + if ($self->_is_lob_type($source->column_info($col)->{data_type})) { + $blob_cols{$col} = delete $fields->{$col}; + $fields->{$col} = \"''"; + } + } + + return \%blob_cols; +} + +sub _update_blobs { + my ($self, $source, $blob_cols, $where) = @_; + + my (@primary_cols) = $source->primary_columns; + + croak "Cannot update TEXT/IMAGE column(s) without a primary key" + unless @primary_cols; + +# check if we're updating a single row by PK + my $pk_cols_in_where = 0; + for my $col (@primary_cols) { + $pk_cols_in_where++ if defined $where->{$col}; + } + my @rows; + + if ($pk_cols_in_where == @primary_cols) { + my %row_to_update; + @row_to_update{@primary_cols} = @{$where}{@primary_cols}; + @rows = \%row_to_update; + } else { + my $rs = $source->resultset->search( + $where, + { + result_class => 'DBIx::Class::ResultClass::HashRefInflator', + select => \@primary_cols + } + ); + @rows = $rs->all; # statement must finish + } + + for my $row (@rows) { + $self->_insert_blobs($source, $blob_cols, $row); + } +} + +sub _insert_blobs { + my ($self, $source, $blob_cols, $row) = @_; + my $dbh = $self->dbh; + + my $table = $source->from; + + my %row = %$row; + my (@primary_cols) = $source->primary_columns; + + croak "Cannot update TEXT/IMAGE column(s) without a primary key" + unless @primary_cols; + + if ((grep { defined $row{$_} } @primary_cols) != @primary_cols) { + if (@primary_cols == 1) { + my $col = $primary_cols[0]; + $row{$col} = $self->last_insert_id($source, $col); + } else { + croak "Cannot update TEXT/IMAGE column(s) without primary key values"; + } + } + + for my $col (keys %$blob_cols) { + 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; + } + + eval { + while ($sth->fetch) { + $sth->func('CS_GET', 1, 'ct_data_info') or die $sth->errstr; + } + $sth->func('ct_prepare_send') or die $sth->errstr; + + my $log_on_update = $self->_blob_log_on_update; + $log_on_update = 1 if not defined $log_on_update; + + $sth->func('CS_SET', 1, { + total_txtlen => length($blob), + log_on_update => $log_on_update + }, 'ct_data_info') or die $sth->errstr; + + $sth->func($blob, length($blob), 'ct_send_data') or die $sth->errstr; + + $sth->func('ct_finish_send') or die $sth->errstr; + }; + my $exception = $@; + $sth->finish; + croak $exception if $exception; + } } =head2 connect_call_datetime_setup @@ -133,79 +438,39 @@ C columns only have minute precision. sub datetime_parser_type { "DateTime::Format::Sybase" } -sub _dbh_last_insert_id { - my ($self, $dbh, $source, $col) = @_; +# savepoint support using ASE syntax - # sorry, there's no other way! - my $sth = $dbh->prepare_cached("select max($col) from ".$source->from); - return ($dbh->selectrow_array($sth))[0]; +sub _svp_begin { + my ($self, $name) = @_; + + $self->dbh->do("SAVE TRANSACTION $name"); } -# previous implementation of limited count for Sybase, does not include -# count_grouped. +# A new SAVE TRANSACTION with the same name releases the previous one. +sub _svp_release { 1 } -#sub _copy_attributes_for_count { -# my ($self, $source, $attrs) = @_; -# my %attrs = %$attrs; -# -# # take off any column specs, any pagers, record_filter is cdbi, and no point of ordering a count -# delete @attrs{qw/select as rows offset page order_by record_filter/}; -# -# return \%attrs; -#} -# -#=head2 count -# -#Counts for limited queries are emulated by executing select queries and -#returning the number of successful executions minus the offset. -# -#This is necessary due to the limitations of Sybase. -# -#=cut -# -#sub count { -# my $self = shift; -# my ($source, $attrs) = @_; -# -# my $new_attrs = $self->_copy_attributes_for_count($source, $attrs); -# -# if (exists $attrs->{rows}) { -# my $offset = $attrs->{offset} || 0; -# my $total = $attrs->{rows} + $offset; -# -# my $first_pk = ($source->primary_columns)[0]; -# -# $new_attrs->{select} = $first_pk ? "me.$first_pk" : 1; -# -# my $tmp_rs = $source->resultset_class->new($source, $new_attrs); -# -# $self->dbh->{syb_rowcount} = $total; -# -# my $count = 0; -# $count++ while $tmp_rs->cursor->next; -# -# $self->dbh->{syb_rowcount} = 0; -# -# return $count - $offset; -# } else { -# # overwrite the selector -# $new_attrs->{select} = { count => '*' }; -# -# my $tmp_rs = $source->resultset_class->new($source, $new_attrs); -# my ($count) = $tmp_rs->cursor->next; -# -# # if the offset/rows attributes are still present, we did not use -# # a subquery, so we need to make the calculations in software -# $count -= $attrs->{offset} if $attrs->{offset}; -# $count = $attrs->{rows} if $attrs->{rows} and $attrs->{rows} < $count; -# $count = 0 if ($count < 0); -# -# return $count; -# } -#} +sub _svp_rollback { + my ($self, $name) = @_; + + $self->dbh->do("ROLLBACK TRANSACTION $name"); +} 1; +=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. + +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. + +See +L +for information on changing the setting on the server side. + =head1 DATES See L to setup date formats @@ -213,12 +478,12 @@ for L. =head1 IMAGE AND TEXT COLUMNS +L compiled with FreeTDS will B work with C +columns. + See L for a L setting you need to work with C columns. -Due to limitations in L and this driver, it is only possible to -select one C or C column at a time. - =head1 AUTHORS See L.