X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FDBIx%2FClass%2FStorage%2FDBI%2FSybase.pm;h=886308d0fad35a0f5f90c4f38b07c2f4a204788b;hb=58e3556d625168736ee3548b60bd0833cb99e69e;hp=f8b5004b99817e436d443b3a3909c8b069ffa89c;hpb=3abafb11007307452da109622919fb9af0979d26;p=dbsrgits%2FDBIx-Class.git diff --git a/lib/DBIx/Class/Storage/DBI/Sybase.pm b/lib/DBIx/Class/Storage/DBI/Sybase.pm index f8b5004..886308d 100644 --- a/lib/DBIx/Class/Storage/DBI/Sybase.pm +++ b/lib/DBIx/Class/Storage/DBI/Sybase.pm @@ -3,17 +3,55 @@ 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::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/ +); + +=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->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 { + 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/ identity//; + + 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 = shift; + my ($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; + $upd_cols; + } + else { + $self->next::method(@_); + } + }; + + $self->_insert_blobs($source, $blob_cols, $to_insert) if %$blob_cols; + + return $updated_cols; +} + +sub update { + my $self = shift; + my ($source, $fields, $where) = @_; + + 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, $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 %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 @@ -63,11 +484,15 @@ 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 ". @@ -75,80 +500,149 @@ C columns only have minute precision. $old_dbd_warned = 1; } - $dbh->do('set dateformat mdy'); + $dbh->do('SET DATEFORMAT mdy'); 1; } } -sub _dbh_last_insert_id { - my ($self, $dbh, $source, $col) = @_; +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.) - # sorry, there's no other way! - my $sth = $dbh->prepare_cached("select max($col) from ".$source->from); - return ($dbh->selectrow_array($sth))[0]; +sub _dbh_begin_work { + my $self = shift; + $self->next::method(@_); + if ($self->using_freetds) { + $self->dbh->do('BEGIN TRAN'); + } } -sub count { +sub _dbh_commit { my $self = shift; - my ($source, $attrs) = @_; + if ($self->using_freetds) { + $self->_dbh->do('COMMIT'); + } + return $self->next::method(@_); +} - if (not exists $attrs->{rows}) { - return $self->next::method(@_); +sub _dbh_rollback { + my $self = shift; + if ($self->using_freetds) { + $self->_dbh->do('ROLLBACK'); } + return $self->next::method(@_); +} - my $offset = $attrs->{offset} || 0; - my $total = $attrs->{rows} + $offset; +# savepoint support using ASE syntax - my $new_attrs = $self->_copy_attributes_for_count($source, $attrs); +sub _svp_begin { + my ($self, $name) = @_; - my $first_pk = ($source->primary_columns)[0]; + $self->dbh->do("SAVE TRANSACTION $name"); +} - $new_attrs->{select} = $first_pk ? "me.$first_pk" : 1; +# A new SAVE TRANSACTION with the same name releases the previous one. +sub _svp_release { 1 } - my $tmp_rs = $source->resultset_class->new($source, $new_attrs); +sub _svp_rollback { + my ($self, $name) = @_; - $self->dbh->{syb_rowcount} = $total; + $self->dbh->do("ROLLBACK TRANSACTION $name"); +} - my $count = 0; - $count++ while $tmp_rs->cursor->next; +1; - $self->dbh->{syb_rowcount} = 0; +=head1 Schema::Loader Support - return $count - $offset; -} +There is an experimental branch of L that will +allow you to dump a schema from most (if not all) versions of Sybase. -sub datetime_parser_type { "DateTime::Format::Sybase" } +It is available via subversion from: -1; + http://dev.catalyst.perl.org/repos/bast/branches/DBIx-Class-Schema-Loader/current/ -=head1 NAME +=head1 FreeTDS -DBIx::Class::Storage::DBI::Sybase - Storage::DBI subclass for Sybase +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: -=head1 SYNOPSIS +The Open Client FAQ is here: +L. -This subclass supports L for real Sybase databases. If you are -using an MSSQL database via L, your storage will be reblessed to -L. +Sybase ASE for Linux (which comes with the Open Client libraries) may be +downloaded here: L. -=head1 DESCRIPTION +To see if you're using FreeTDS check C<< $schema->storage->using_freetds >>, or run: -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. + perl -MDBI -le 'my $dbh = DBI->connect($dsn, $user, $pass); print $dbh->{syb_oc_version}' -With this driver there is unfortunately no way to get the C -without doing a C