X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FDBIx%2FClass%2FStorage%2FDBI%2FSybase.pm;h=02464e4216a08cf37c594996a0c8c2cc6f8d2ac2;hb=56ad42bb48befbeb50953d197e7cb86bbc62686c;hp=e28ee63cf0155997473ec4bc6c4098671f2c9da7;hpb=0ac07712a87c97ec1676410be95ddfef768bfe23;p=dbsrgits%2FDBIx-Class.git diff --git a/lib/DBIx/Class/Storage/DBI/Sybase.pm b/lib/DBIx/Class/Storage/DBI/Sybase.pm index e28ee63..02464e4 100644 --- a/lib/DBIx/Class/Storage/DBI/Sybase.pm +++ b/lib/DBIx/Class/Storage/DBI/Sybase.pm @@ -2,45 +2,20 @@ package DBIx::Class::Storage::DBI::Sybase; use strict; use warnings; +use Try::Tiny; +use namespace::clean; -use base qw/ - DBIx::Class::Storage::DBI::Sybase::Common - DBIx::Class::Storage::DBI::AutoCast -/; -use mro 'c3'; -use Carp::Clan qw/^DBIx::Class/; -use List::Util (); - -__PACKAGE__->mk_group_accessors('simple' => - qw/_identity _blob_log_on_update insert_txn/ -); +use base qw/DBIx::Class::Storage::DBI/; =head1 NAME -DBIx::Class::Storage::DBI::Sybase - Sybase support for DBIx::Class - -=head1 SYNOPSIS - -This subclass supports L for real Sybase databases. If you are -using an MSSQL database via L, your storage will be reblessed to -L. +DBIx::Class::Storage::DBI::Sybase - Base class for drivers using +L =head1 DESCRIPTION -If your version of Sybase does not support placeholders, then your storage -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 when placeholders are enabled. - -When using C transactions are -disabled. - -To turn off transactions for inserts (for an application that doesn't need -concurrency, or a loader, for example) use this setting in -L, - - on_connect_call => ['unsafe_insert'] - -To manipulate this setting at runtime, use: - - $schema->storage->insert_txn(0); # 1 to re-enable - -=cut - -sub connect_call_unsafe_insert { - my $self = shift; - $self->insert_txn(0); -} - -sub _is_lob_type { - my $self = shift; - my $type = shift; - $type && $type =~ /(?:text|image|lob|bytea|binary|memo)/i; -} - -sub _prep_for_execute { +sub _init { + # once the driver is determined see if we need to insert the DBD::Sybase w/ FreeTDS fixups + # this is a dirty version of "instance role application", \o/ DO WANT Moo \o/ 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 = join ("\n", - "SET IDENTITY_INSERT $table ON", - $sql, - "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); - } - } + if (! $self->isa('DBIx::Class::Storage::DBI::Sybase::FreeTDS') and $self->_using_freetds) { + require DBIx::Class::Storage::DBI::Sybase::FreeTDS; - return ($sql, $bind); -} - -# Stolen from SQLT, with some modifications. This is a makeshift -# solution before a sane type-mapping library is available, thus -# the 'our' for easy overrides. -our %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 _native_data_type { - my ($self, $type) = @_; - - $type = lc $type; - $type =~ s/ identity//; - - return uc($TYPE_MAPPING{$type} || $type); -} + my @isa = @{mro::get_linear_isa(ref $self)}; + my $class = shift @isa; # this is our current ref -sub _fetch_identity_sql { - my ($self, $source, $col) = @_; - - return "SELECT MAX($col) FROM ".$source->from; -} + my $trait_class = $class . '::FreeTDS'; + mro::set_mro ($trait_class, 'c3'); + no strict 'refs'; + @{"${trait_class}::ISA"} = ($class, 'DBIx::Class::Storage::DBI::Sybase::FreeTDS', @isa); -sub _execute { - my $self = shift; - my ($op) = @_; + bless ($self, $trait_class); - my ($rv, $sth, @bind) = $self->dbh_do($self->can('_dbh_execute'), @_); + Class::C3->reinitialize() if DBIx::Class::_ENV_::OLD_MRO; - if ($op eq 'insert') { - $self->_identity($sth->fetchrow_array); - $sth->finish; + $self->_init(@_); } - 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 = shift; - my ($ident, $source, $to_insert) = @_; - - my $blob_cols = $self->_remove_blob_cols($source, $to_insert); - - my $need_last_insert_id = 0; - - my ($identity_col) = - map $_->[0], - grep $_->[1]{is_auto_increment}, - map [ $_, $source->column_info($_) ], - $source->columns; - - $need_last_insert_id = 1 - if $identity_col && (not exists $to_insert->{$identity_col}); - - # 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 ($need_last_insert_id && $self->insert_txn && - (not $self->{transaction_depth})) { - my $guard = $self->txn_scope_guard; - my $upd_cols = $self->next::method (@_); - $guard->commit; - return $upd_cols; - } - else { - $self->next::method(@_); - } - }; - - $self->_insert_blobs($source, $blob_cols, $to_insert) if %$blob_cols; - - return $updated_cols; + $self->next::method(@_); } -sub update { +sub _ping { my $self = shift; - my ($source, $fields, $ident_cond) = @_; - - my $wantarray = wantarray; - - my $blob_cols = $self->_remove_blob_cols($source, $fields); - - my @res; - if ($wantarray) { - @res = $self->next::method(@_); - } - elsif (defined $wantarray) { - $res[0] = $self->next::method(@_); - } - else { - $self->next::method(@_); - } - - $self->_update_blobs($source, $blob_cols, $ident_cond) 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, $ident_cond) = @_; - - 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 $ident_cond->{$col}; - } - my @rows; - - if ($pk_cols_in_where == @primary_cols) { - my %row_to_update; - @row_to_update{@primary_cols} = @{$ident_cond}{@primary_cols}; - @rows = \%row_to_update; - } else { - my $rs = $source->resultset->search( - $ident_cond, - { - 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); - } -} + my $dbh = $self->_dbh or return 0; -sub _insert_blobs { - my ($self, $source, $blob_cols, $row) = @_; - my $dbh = $self->dbh; + local $dbh->{RaiseError} = 1; + local $dbh->{PrintError} = 0; - my $table = $source->from; +# FIXME if the main connection goes stale, does opening another for this statement +# really determine anything? - 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"; + if ($dbh->{syb_no_child_con}) { + return try { + $self->_connect(@{$self->_dbi_connect_info || [] }) + ->do('select 1'); + 1; } - } - - for my $col (keys %$blob_cols) { - my $blob = $blob_cols->{$col}; - - my %where = map { ($_, $row{$_}) } @primary_cols; - my $cursor = $source->resultset->search(\%where, { - select => [$col] - })->cursor; - $cursor->next; - my $sth = $cursor->sth; - - eval { - 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; - $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; + catch { + 0; }; - my $exception = $@; - $sth->finish if $sth; - if ($exception) { - if ($self->using_freetds) { - croak ( - 'TEXT/IMAGE operation failed, probably because you are using FreeTDS: ' - . $exception - ); - } else { - croak $exception; - } - } } -} - -=head2 connect_call_datetime_setup - -Used as: - - on_connect_call => 'datetime_setup' - -In L to set: - - $dbh->syb_date_fmt('ISO_strict'); # output fmt: 2004-08-21T14:36:48.080Z - $dbh->do('set dateformat mdy'); # input fmt: 08/13/1979 18:08:55.080 - -On connection for use with L, using -L, which you will need to install. - -This works for both C and C columns, although -C columns only have minute precision. - -=cut - -{ - my $old_dbd_warned = 0; - - sub connect_call_datetime_setup { - my $self = shift; - 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 ". - "DBIx::Class::InflateColumn::DateTime, please upgrade!"; - $old_dbd_warned = 1; - } - - $dbh->do('SET DATEFORMAT mdy'); + return try { + $dbh->do('select 1'); 1; } + catch { + 0; + }; } -sub datetime_parser_type { "DateTime::Format::Sybase" } +sub _set_max_connect { + my $self = shift; + my $val = shift || 256; -# ->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.) + my $dsn = $self->_dbi_connect_info->[0]; -sub _dbh_begin_work { - my $self = shift; - $self->next::method(@_); - if ($self->using_freetds) { - $self->dbh->do('BEGIN TRAN'); - } -} + return if ref($dsn) eq 'CODE'; -sub _dbh_commit { - my $self = shift; - if ($self->using_freetds) { - $self->_dbh->do('COMMIT'); + if ($dsn !~ /maxConnect=/) { + $self->_dbi_connect_info->[0] = "$dsn;maxConnect=$val"; + my $connected = defined $self->_dbh; + $self->disconnect; + $self->ensure_connected if $connected; } - return $self->next::method(@_); } -sub _dbh_rollback { +# Whether or not DBD::Sybase was compiled against FreeTDS. If false, it means +# the Sybase OpenClient libraries were used. +sub _using_freetds { my $self = shift; - if ($self->using_freetds) { - $self->_dbh->do('ROLLBACK'); - } - return $self->next::method(@_); + return ($self->_get_dbh->{syb_oc_version}||'') =~ /freetds/i; } -# 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 } - -sub _svp_rollback { - my ($self, $name) = @_; - - $self->dbh->do("ROLLBACK TRANSACTION $name"); +# Either returns the FreeTDS version against which DBD::Sybase was compiled, +# 0 if can't be determined, or undef otherwise +sub _using_freetds_version { + my $inf = shift->_get_dbh->{syb_oc_version}; + return undef unless ($inf||'') =~ /freetds/i; + return $inf =~ /v([0-9\.]+)/ ? $1 : 0; } 1; -=head1 Schema::Loader Support - -There is an experimental branch of L that will -allow you to dump a schema from most (if not all) versions of Sybase. - -It is available via subversion from: - - http://dev.catalyst.perl.org/repos/bast/branches/DBIx-Class-Schema-Loader/current/ - -=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 type -conversion errors for anything that's not expecting a string. In such a case, -the C option from L is -automatically set, which you may enable on connection with -L. The type info -for the Cs is taken from the L -definitions in your Result classes, and are mapped to a Sybase type (if it isn't -already) using a mapping based on L. - -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 - -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 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 -for information on changing the setting on the server side. - -=head1 DATES - -See L to setup date formats -for L. - -=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. - -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. - =head1 AUTHORS See L. @@ -651,4 +143,3 @@ See L. You may distribute this code under the same terms as Perl itself. =cut -# vim:sts=2 sw=2: