Merge 'trunk' into 'oracle-tweaks'
Peter Rabbitson [Fri, 8 May 2009 17:19:25 +0000 (17:19 +0000)]
r6114@Thesaurus (orig r6113):  ribasushi | 2009-05-03 10:23:28 +0200
Bump SQLA ependencies so parenthesis_significant is guaranteed to be there
r6127@Thesaurus (orig r6126):  arcanez | 2009-05-05 09:27:56 +0200
 r6037@mullet (orig r6036):  arcanez | 2009-04-30 01:24:41 -0700
 branch to work on Sybase/MSSQL subtleties
 r6038@mullet (orig r6037):  arcanez | 2009-04-30 01:27:11 -0700
 jump to ::DBI::Sybase::MSSQL if we are using MSSQL through Sybase
 r6062@mullet (orig r6061):  arcanez | 2009-04-30 14:05:24 -0700
 fixes for MSSQL via Sybase
 r6126@mullet (orig r6125):  arcanez | 2009-05-05 00:27:13 -0700
 add a line to Changes and add a CAVEAT

r6164@Thesaurus (orig r6163):  ribasushi | 2009-05-07 19:09:01 +0200
 r6115@Thesaurus (orig r6114):  plu | 2009-05-03 10:39:16 +0200
 new branch to fix $rs->update and $rs->delete using the new as_query method

 r6116@Thesaurus (orig r6115):  plu | 2009-05-03 10:52:07 +0200
 Methods update/delete on resultset use now new as_query method to updated/delete properly on joined/prefetched resultset using a subquery. Therefore some tests have been added and some have been changed as well as the warnings around $rs->update/delete have been removed. Cheers!
 r6117@Thesaurus (orig r6116):  plu | 2009-05-03 11:13:48 +0200
 Using "is" instead of "cmp_ok"
 r6160@Thesaurus (orig r6159):  ribasushi | 2009-05-07 11:58:14 +0200
 Back out skip_parens support in as_query
 r6161@Thesaurus (orig r6160):  ribasushi | 2009-05-07 19:00:48 +0200
 This test is completely borked, needs a rewrite
 r6162@Thesaurus (orig r6161):  ribasushi | 2009-05-07 19:07:19 +0200
 Temporary fix or the IN ( ( ... ) ) problem until we get proper SQLA AST (needs SQLA released with commit 6158 to work)

r6165@Thesaurus (orig r6164):  ribasushi | 2009-05-07 19:11:46 +0200
Changes, remove merged branch
r6169@Thesaurus (orig r6168):  ribasushi | 2009-05-07 19:24:54 +0200
Bump SQLA dependency so -in/-between workarounds overload properly
r6172@Thesaurus (orig r6171):  ribasushi | 2009-05-07 20:49:26 +0200
Cookbook cleanup
r6174@Thesaurus (orig r6173):  ribasushi | 2009-05-08 10:13:30 +0200
Throw away some debugging code
r6175@Thesaurus (orig r6174):  ribasushi | 2009-05-08 10:21:53 +0200
Documentation patch by nniuq
r6176@Thesaurus (orig r6175):  plu | 2009-05-08 10:30:20 +0200
Set NLS_LANG so we  have a predictable date format when using MON
r6177@Thesaurus (orig r6176):  ribasushi | 2009-05-08 12:15:15 +0200
Fix POD
r6179@Thesaurus (orig r6178):  jgoulah | 2009-05-08 16:27:49 +0200
renaming rh performance test so it will show up at the end of test output

lib/DBIx/Class.pm
lib/DBIx/Class/InflateColumn/DateTime.pm
lib/DBIx/Class/Storage/DBI/Oracle/Generic.pm
t/73oracle.t
t/73oracle_inflate.t
t/lib/DBICTest/Schema/Track.pm
t/lib/sqlite.sql

index 32b58a1..506a8d7 100644 (file)
@@ -275,6 +275,8 @@ nigel: Nigel Metheringham <nigelm@cpan.org>
 
 ningu: David Kamholz <dkamholz@cpan.org>
 
+Nniuq: Ron "Quinn" Straight" <quinnfazigu@gmail.org>
+
 norbi: Norbert Buchmuller <norbi@nix.hu>
 
 Numa: Dan Sully <daniel@cpan.org>
index afd118d..02aec53 100644 (file)
@@ -94,7 +94,7 @@ sub register_column {
 
   my $type;
 
-  for (qw/date datetime/) {
+  for (qw/date datetime timestamp/) {
     my $key = "inflate_${_}";
 
     next unless exists $info->{$key};
@@ -106,7 +106,6 @@ sub register_column {
 
   unless ($type) {
     $type = lc($info->{data_type});
-    $type = 'datetime' if ($type =~ /^timestamp/);
   }
 
   my $timezone;
@@ -128,36 +127,23 @@ sub register_column {
 
   my $undef_if_invalid = $info->{datetime_undef_if_invalid};
 
-  if ($type eq 'datetime' || $type eq 'date') {
-    my ($parse, $format) = ("parse_${type}", "format_${type}");
-
-    # This assignment must happen here, otherwise Devel::Cycle treats
-    # the resulting deflator as a circular reference (go figure):
-    #
-    # Cycle #1
-    #     DBICTest::Schema A->{source_registrations} => %B
-    #     %B->{Event} => DBIx::Class::ResultSource::Table C
-    #     DBIx::Class::ResultSource::Table C->{_columns} => %D
-    #     %D->{created_on} => %E
-    #     %E->{_inflate_info} => %F
-    #     %F->{deflate} => &G
-    #     closure &G, $info => $H
-    #     $H => %E
-    #
-    my $floating_tz_ok;
+  if ($type eq 'datetime' || $type eq 'date' || $type eq 'timestamp') {
+    # This shallow copy of %info avoids t/52_cycle.t treating
+    # the resulting deflator as a circular reference.
+    my %info = ( '_ic_dt_method' => $type , %{ $info } );
+
     if (defined $info->{extra}{floating_tz_ok}) {
       warn "Putting floating_tz_ok into extra => { floating_tz_ok => 1 } has been deprecated, ".
            "please put it directly into the columns definition.";
-      $floating_tz_ok = $info->{extra}{floating_tz_ok};
+      $info{floating_tz_ok} = $info->{extra}{floating_tz_ok};
     }
-    $floating_tz_ok = $info->{floating_tz_ok} if defined $info->{floating_tz_ok};
 
     $self->inflate_column(
       $column =>
         {
           inflate => sub {
             my ($value, $obj) = @_;
-            my $dt = eval { $obj->_datetime_parser->$parse($value); };
+            my $dt = eval { $obj->_inflate_to_datetime( $value, \%info ) };
             die "Error while inflating ${value} for ${column} on ${self}: $@"
               if $@ and not $undef_if_invalid;
             $dt->set_time_zone($timezone) if $timezone;
@@ -170,18 +156,38 @@ sub register_column {
                 warn "You're using a floating timezone, please see the documentation of"
                   . " DBIx::Class::InflateColumn::DateTime for an explanation"
                   if ref( $value->time_zone ) eq 'DateTime::TimeZone::Floating'
-                      and not $floating_tz_ok
+                      and not $info{floating_tz_ok}
                       and not $ENV{DBIC_FLOATING_TZ_OK};
                 $value->set_time_zone($timezone);
                 $value->set_locale($locale) if $locale;
             }
-            $obj->_datetime_parser->$format($value);
+            $obj->_deflate_from_datetime( $value, \%info );
           },
         }
     );
   }
 }
 
+sub _flate_or_fallback
+{
+  my( $self, $value, $info, $method_fmt ) = @_;
+
+  my $parser = $self->_datetime_parser;
+  my $preferred_method = sprintf($method_fmt, $info->{ _ic_dt_method });
+  my $method = $parser->can($preferred_method) ? $preferred_method : sprintf($method_fmt, 'datetime');
+  return $parser->$method($value);
+}
+
+sub _inflate_to_datetime {
+  my( $self, $value, $info ) = @_;
+  return $self->_flate_or_fallback( $value, $info, 'parse_%s' );
+}
+
+sub _deflate_from_datetime {
+  my( $self, $value, $info ) = @_;
+  return $self->_flate_or_fallback( $value, $info, 'format_%s' );
+}
+
 sub _datetime_parser {
   my $self = shift;
   if (my $parser = $self->__datetime_parser) {
index 2e9a8c1..27497e6 100644 (file)
@@ -26,6 +26,9 @@ This class implements autoincrements for Oracle.
 
 use Carp::Clan qw/^DBIx::Class/;
 
+# For ORA_BLOB => 113, ORA_CLOB => 112
+use DBD::Oracle qw( :ora_types );
+
 use base qw/DBIx::Class::Storage::DBI::MultiDistinctEmulation/;
 
 # __PACKAGE__->load_components(qw/PK::Auto/);
@@ -190,6 +193,48 @@ sub _svp_begin {
     $self->dbh->do("SAVEPOINT $name");
 }
 
+=head2 source_bind_attributes
+
+Handle LOB types in Oracle.  Under a certain size (4k?), you can get away
+with the driver assuming your input is the deprecated LONG type if you
+encode it as a hex string.  That ain't gonna fly at larger values, where
+you'll discover you have to do what this does.
+
+This method had to be overridden because we need to set ora_field to the
+actual column, and that isn't passed to the call (provided by Storage) to
+bind_attribute_by_data_type.
+
+According to L<DBD::Oracle>, the ora_field isn't always necessary, but
+adding it doesn't hurt, and will save your bacon if you're modifying a
+table with more than one LOB column.
+
+=cut
+
+sub source_bind_attributes 
+{
+       my $self = shift;
+       my($source) = @_;
+
+       my %bind_attributes;
+
+       foreach my $column ($source->columns) {
+               my $data_type = $source->column_info($column)->{data_type} || '';
+               next unless $data_type;
+
+               my %column_bind_attrs = $self->bind_attribute_by_data_type($data_type);
+
+               if ($data_type =~ /^[BC]LOB$/i) {
+                       $column_bind_attrs{'ora_type'}
+                               = uc($data_type) eq 'CLOB' ? ORA_CLOB : ORA_BLOB;
+                       $column_bind_attrs{'ora_field'} = $column;
+               }
+
+               $bind_attributes{$column} = \%column_bind_attrs;
+       }
+
+       return \%bind_attributes;
+}
+
 # Oracle automatically releases a savepoint when you start another one with the
 # same name.
 sub _svp_release { 1 }
index 51cc932..fe69024 100644 (file)
@@ -28,6 +28,7 @@
 use strict;
 use warnings;  
 
+use Test::Exception;
 use Test::More;
 use lib qw(t/lib);
 use DBICTest;
@@ -39,7 +40,7 @@ plan skip_all => 'Set $ENV{DBICTEST_ORA_DSN}, _USER and _PASS to run this test.
   ' as well as following sequences: \'pkid1_seq\', \'pkid2_seq\' and \'nonpkid_seq\''
   unless ($dsn && $user && $pass);
 
-plan tests => 24;
+plan tests => 32;
 
 DBICTest::Schema->load_classes('ArtistFQN');
 my $schema = DBICTest::Schema->connect($dsn, $user, $pass);
@@ -63,7 +64,7 @@ $dbh->do("CREATE SEQUENCE nonpkid_seq START WITH 20 MAXVALUE 999999 MINVALUE 0")
 $dbh->do("CREATE TABLE artist (artistid NUMBER(12), name VARCHAR(255), rank NUMBER(38), charfield VARCHAR2(10))");
 $dbh->do("CREATE TABLE sequence_test (pkid1 NUMBER(12), pkid2 NUMBER(12), nonpkid NUMBER(12), name VARCHAR(255))");
 $dbh->do("CREATE TABLE cd (cdid NUMBER(12), artist NUMBER(12), title VARCHAR(255), year VARCHAR(4))");
-$dbh->do("CREATE TABLE track (trackid NUMBER(12), cd NUMBER(12), position NUMBER(12), title VARCHAR(255), last_updated_on DATE)");
+$dbh->do("CREATE TABLE track (trackid NUMBER(12), cd NUMBER(12), position NUMBER(12), title VARCHAR(255), last_updated_on DATE, last_updated_at DATE)");
 
 $dbh->do("ALTER TABLE artist ADD (CONSTRAINT artist_pk PRIMARY KEY (artistid))");
 $dbh->do("ALTER TABLE sequence_test ADD (CONSTRAINT sequence_test_constraint PRIMARY KEY (pkid1, pkid2))");
@@ -80,6 +81,23 @@ $dbh->do(qq{
   END;
 });
 
+{
+    # Swiped from t/bindtype_columns.t to avoid creating my own Resultset.
+
+    local $SIG{__WARN__} = sub {};
+    eval { $dbh->do('DROP TABLE bindtype_test') };
+
+    $dbh->do(qq[
+        CREATE TABLE bindtype_test 
+        (
+            id              integer      NOT NULL   PRIMARY KEY,
+            bytea           integer      NULL,
+            blob            blob         NULL,
+            clob            clob         NULL
+        )
+    ],{ RaiseError => 1, PrintError => 1 });
+}
+
 # This is in Core now, but it's here just to test that it doesn't break
 $schema->class('Artist')->load_components('PK::Auto');
 # These are compat shims for PK::Auto...
@@ -147,6 +165,29 @@ for (1..5) {
 my $st = $schema->resultset('SequenceTest')->create({ name => 'foo', pkid1 => 55 });
 is($st->pkid1, 55, "Oracle Auto-PK without trigger: First primary key set manually");
 
+{
+       my %binstr = ( 'small' => join('', map { chr($_) } ( 1 .. 127 )) );
+       $binstr{'large'} = $binstr{'small'} x 1024;
+
+       my $maxloblen = length $binstr{'large'};
+       note "Localizing LongReadLen to $maxloblen to avoid truncation of test data";
+       local $dbh->{'LongReadLen'} = $maxloblen;
+
+       my $rs = $schema->resultset('BindType');
+       my $id = 0;
+
+       foreach my $type (qw( blob clob )) {
+               foreach my $size (qw( small large )) {
+                       $id++;
+
+                       lives_ok { $rs->create( { 'id' => $id, $type => $binstr{$type} } ) }
+                               "inserted $size $type without dying";
+
+                       is( $rs->find($id)->$type, $binstr{$type}, "verified inserted $size $type" );
+               }
+       }
+}
+
 # clean up our mess
 END {
     if($schema && ($dbh = $schema->storage->dbh)) {
@@ -158,6 +199,7 @@ END {
         $dbh->do("DROP TABLE sequence_test");
         $dbh->do("DROP TABLE cd");
         $dbh->do("DROP TABLE track");
+        $dbh->do("DROP TABLE bindtype_test");
     }
 }
 
index 22e1d62..0f2fc23 100644 (file)
@@ -17,12 +17,13 @@ else {
         plan skip_all => 'needs DateTime and DateTime::Format::Oracle for testing';
     }
     else {
-        plan tests => 4;
+        plan tests => 7;
     }
 }
 
 # DateTime::Format::Oracle needs this set
 $ENV{NLS_DATE_FORMAT} = 'DD-MON-YY';
+$ENV{NLS_TIMESTAMP_FORMAT} = 'YYYY-MM-DD HH24:MI:SSXFF';
 $ENV{NLS_LANG} = 'AMERICAN_AMERICA.WE8ISO8859P1';
 
 my $schema = DBICTest::Schema->connect($dsn, $user, $pass);
@@ -31,16 +32,20 @@ my $schema = DBICTest::Schema->connect($dsn, $user, $pass);
 my $col_metadata = $schema->class('Track')->column_info('last_updated_on');
 $schema->class('Track')->add_column( 'last_updated_on' => {
     data_type => 'date' });
+$schema->class('Track')->add_column( 'last_updated_at' => {
+    data_type => 'timestamp' });
 
 my $dbh = $schema->storage->dbh;
 
+#$dbh->do("alter session set nls_timestamp_format = 'YYYY-MM-DD HH24:MI:SSXFF'");
+
 eval {
   $dbh->do("DROP TABLE track");
 };
-$dbh->do("CREATE TABLE track (trackid NUMBER(12), cd NUMBER(12), position NUMBER(12), title VARCHAR(255), last_updated_on DATE)");
+$dbh->do("CREATE TABLE track (trackid NUMBER(12), cd NUMBER(12), position NUMBER(12), title VARCHAR(255), last_updated_on DATE, last_updated_at TIMESTAMP)");
 
 # insert a row to play with
-my $new = $schema->resultset('Track')->create({ trackid => 1, cd => 1, position => 1, title => 'Track1', last_updated_on => '06-MAY-07' });
+my $new = $schema->resultset('Track')->create({ trackid => 1, cd => 1, position => 1, title => 'Track1', last_updated_on => '06-MAY-07', last_updated_at => '2009-05-03 21:17:18.5' });
 is($new->trackid, 1, "insert sucessful");
 
 my $track = $schema->resultset('Track')->find( 1 );
@@ -49,11 +54,18 @@ is( ref($track->last_updated_on), 'DateTime', "last_updated_on inflated ok");
 
 is( $track->last_updated_on->month, 5, "DateTime methods work on inflated column");
 
+#note '$track->last_updated_at => ', $track->last_updated_at;
+is( ref($track->last_updated_at), 'DateTime', "last_updated_at inflated ok");
+
+is( $track->last_updated_at->nanosecond, 500_000_000, "DateTime methods work with nanosecond precision");
+
 my $dt = DateTime->now();
 $track->last_updated_on($dt);
+$track->last_updated_at($dt);
 $track->update;
 
 is( $track->last_updated_on->month, $dt->month, "deflate ok");
+is( int $track->last_updated_at->nanosecond, int $dt->nanosecond, "deflate ok with nanosecond precision");
 
 # clean up our mess
 END {
index e3f731e..ee07f5e 100644 (file)
@@ -26,6 +26,10 @@ __PACKAGE__->add_columns(
     accessor => 'updated_date',
     is_nullable => 1
   },
+  last_updated_at => {
+    data_type => 'datetime',
+    is_nullable => 1
+  },
 );
 __PACKAGE__->set_primary_key('trackid');
 
index 9e8aaa6..0eef182 100644 (file)
@@ -375,7 +375,8 @@ CREATE TABLE track (
   cd integer NOT NULL,
   position integer NOT NULL,
   title varchar(100) NOT NULL,
-  last_updated_on datetime
+  last_updated_on datetime,
+  last_updated_at datetime
 );
 
 CREATE INDEX track_idx_cd_track ON track (cd);