X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FDBIx%2FClass%2FStorage%2FDBI%2FSybase.pm;h=41b0c81bb297e2557717b25f779ed064c3832335;hb=26283ee38f220f6c6bae720ea5a189c9c0f47f6f;hp=6c0736983c77fa8e1b621be7425b8411ef208c84;hpb=c9d9c67046115c4717b9ad49532b66d82d135310;p=dbsrgits%2FDBIx-Class.git diff --git a/lib/DBIx/Class/Storage/DBI/Sybase.pm b/lib/DBIx/Class/Storage/DBI/Sybase.pm index 6c07369..41b0c81 100644 --- a/lib/DBIx/Class/Storage/DBI/Sybase.pm +++ b/lib/DBIx/Class/Storage/DBI/Sybase.pm @@ -4,694 +4,63 @@ use strict; use warnings; use base qw/ - DBIx::Class::Storage::DBI::Sybase::Common - DBIx::Class::Storage::DBI::AutoCast + DBIx::Class::Storage::DBI::Sybase::Base + DBIx::Class::Storage::DBI::NoBindVars /; use mro 'c3'; -use Carp::Clan qw/^DBIx::Class/; -use List::Util (); - -__PACKAGE__->mk_group_accessors('simple' => - qw/_identity _blob_log_on_update unsafe_insert _insert_dbh/ -); - -=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. - -=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->unsafe_insert(0|1); - -=cut - -sub connect_call_unsafe_insert { - my $self = shift; - $self->unsafe_insert(1); -} - -sub _is_lob_type { - my $self = shift; - my $type = shift; - $type && $type =~ /(?:text|image|lob|bytea|binary|memo)/i; -} - -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 = 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); - } - } - - 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/\s* identity//x; - - 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 } - -# handles TEXT/IMAGE and transaction for last_insert_id -sub insert { - my $self = shift; - my ($source, $to_insert) = @_; - - my $blob_cols = $self->_remove_blob_cols($source, $to_insert); - -# insert+blob insert done atomically - my $guard = $self->txn_scope_guard if $blob_cols; - - 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->unsafe_insert && !$self->{transaction_depth} - ) { - local $self->{_dbh} = $self->_insert_dbh; - my $guard = $self->txn_scope_guard; - my $upd_cols = $self->next::method (@_); - $guard->commit; - $upd_cols; - } - else { - $self->next::method(@_); - } - }; - - $self->_insert_blobs($source, $blob_cols, $to_insert) if $blob_cols; - - $guard->commit if $guard; - - return $updated_cols; -} - -sub update { - my $self = shift; - my ($source, $fields, $where) = @_; - - my $wantarray = wantarray; - - my $blob_cols = $self->_remove_blob_cols($source, $fields); - -# update+blob update(s) done atomically - my $guard = $self->txn_scope_guard if $blob_cols; - - 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, $where) if $blob_cols; - - $guard->commit if $guard; - - 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 keys %blob_cols ? \%blob_cols : undef; -} - -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->_get_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 %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; - }; - 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'); - - 1; - } -} - -sub datetime_parser_type { "DateTime::Format::Sybase" } - -# ->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.) - -sub _dbh_begin_work { - my $self = shift; - $self->next::method(@_); - if ($self->using_freetds) { - $self->_get_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->_get_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->_get_dbh->do("ROLLBACK TRANSACTION $name"); +sub _dbh_last_insert_id { + my ($self, $dbh, $source, $col) = @_; + return ($dbh->selectrow_array('select @@identity'))[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 TRANSACTIONS - -Due to limitations of the TDS protocol, L, or both; you cannot -begin a transaction while there are active cursors. An active cursor is, for -example, a L that has been executed using -C or C but has not been exhausted or -L. - -Transactions done for inserts in C mode when placeholders are in use -are also affected, so this won't work: - - while (my $row = $rs1->next) { - $rs2->create({ foo => $row->foo }); - } - -Some workarounds: - -=over 4 - -=item * set C<< $schema->storage->unsafe_insert(1) >> temporarily (see -L) - -=item * use L - -=item * L another L - -=item * load the data from your cursor with L - -=item * enlarge the scope of the transaction - -=back - -=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. +=head1 NAME -Setting C<< $dbh->{LongReadLen} >> will also not work with FreeTDS use either: +DBIx::Class::Storage::DBI::Sybase - Storage::DBI subclass for Sybase - $schema->storage->dbh->do("SET TEXTSIZE $bytes"); +=head1 SYNOPSIS -or +This subclass supports L for real Sybase databases. If +you are using an MSSQL database via L, see +L. - $schema->storage->set_textsize($bytes); +=head1 CAVEATS -instead. +This storage driver uses L as a base. +This means that bind variables will be interpolated (properly quoted of course) +into the SQL query itself, without using bind placeholders. -However, the C you pass in -L is used to execute the equivalent -C command on connection. +More importantly this means that caching of prepared statements is explicitly +disabled, as the interpolation renders it useless. -See L for a L -setting you need to work with C columns. +=head1 AUTHORS -=head1 AUTHOR +Brandon L Black -See L. +Justin Hunter =head1 LICENSE You may distribute this code under the same terms as Perl itself. =cut -# vim:sts=2 sw=2: