Oracle: fix LOB conversions for non-LOBs (RT#69548)
Rafael Kitover [Mon, 18 Jul 2011 16:28:48 +0000 (12:28 -0400)]
Recalculates the LOB binds index for UPDATE queries, as it was
erroneosly not recalculated before. This resulted in non-LOB columns
being treated as LOBs by the SQL mangling code.

ribasushi also optimizes the code for the predominant case of queries
without LOBs in the WHERE clause.

Changes
lib/DBIx/Class/Storage/DBI/Oracle/Generic.pm
t/73oracle.t

diff --git a/Changes b/Changes
index a9034d0..d8b4f58 100644 (file)
--- a/Changes
+++ b/Changes
@@ -4,6 +4,8 @@ Revision history for DBIx::Class
         - Fix $rs->populate([]) to be a no-op rather than an exception
         - Overhaul t/53lean_startup.t to better dodge false positives
         - Stop Data::Compare from loading random plugins
+        - Oracle: Recalculate LOB bind indices for UPDATE with LOBs in WHERE
+          (RT#69548)
 
 0.08193 2011-07-14 17:00 (UTC)
     * New Features / Changes
index 81dbeed..cfefc77 100644 (file)
@@ -495,94 +495,101 @@ sub _prep_for_execute {
   my $self = shift;
   my ($op) = @_;
 
-  my ($sql, $bind) = $self->next::method(@_);
+  return $self->next::method(@_)
+    if $op eq 'insert';
 
-  return ($sql, $bind) if $op eq 'insert';
+  my ($sql, $bind) = $self->next::method(@_);
 
-  my $blob_bind_index;
-  for (0 .. $#$bind) {
-    $blob_bind_index->{$_} = 1 if $self->_is_lob_type(
+  my $lob_bind_indices = { map {
+    (
       $bind->[$_][0]{sqlt_datatype}
-    );
-  }
+        and
+      $self->_is_lob_type($bind->[$_][0]{sqlt_datatype})
+    ) ? ( $_ => 1 ) : ()
+  } ( 0 .. $#$bind ) };
 
-  return ($sql, $bind) unless $blob_bind_index;
+  return ($sql, $bind) unless %$lob_bind_indices;
 
-  my (@sql_parts, $new_sql, @new_binds);
-
-  if ($op eq 'select' || $op eq 'delete') {
-    @sql_parts = split /\?/, $sql;
-  }
-  elsif ($op eq 'update') {
+  my ($final_sql, @final_binds);
+  if ($op eq 'update') {
     $self->throw_exception('Update with complex WHERE clauses currently not supported')
       if $sql =~ /\bWHERE\b .+ \bWHERE\b/xs;
 
-    my ($set_part, $where_part) = $sql =~ /^ (.+?) ( \bWHERE\b .+) /xs;
+    ($final_sql, $sql) = $sql =~ /^ (.+?) ( \bWHERE\b .+) /xs;
+
+    if (my $set_bind_count = $final_sql =~ y/?//) {
 
-    my $set_bind_count = $set_part =~ y/?//;
-    @new_binds = splice @$bind, 0, $set_bind_count;
+      delete $lob_bind_indices->{$_} for (0 .. ($set_bind_count - 1));
 
-    @sql_parts = split /\?/, $where_part;
-    $new_sql  = $set_part;
+      # bail if only the update part contains blobs
+      return ($sql, $bind) unless %$lob_bind_indices;
+
+      @final_binds = splice @$bind, 0, $set_bind_count;
+      $lob_bind_indices = { map
+        { $_ - $set_bind_count => $lob_bind_indices->{$_} }
+        keys %$lob_bind_indices
+      };
+    }
   }
-  else {
+  elsif ($op ne 'select' and $op ne 'delete') {
     $self->throw_exception("Unsupported \$op: $op");
   }
 
+  my @sql_parts = split /\?/, $sql;
+
   my $col_equality_re = qr/ (?<=\s) ([\w."]+) (\s*=\s*) $/x;
 
   for my $b_idx (0 .. $#$bind) {
     my $bound = $bind->[$b_idx];
 
-    if ($blob_bind_index->{$b_idx}) {
-      if (my ($col, $eq) = $sql_parts[0] =~ $col_equality_re) {
-        my $data = $bound->[1];
-
-        $data = "$data" if ref $data;
+    if (
+      $lob_bind_indices->{$b_idx}
+        and
+      my ($col, $eq) = $sql_parts[0] =~ $col_equality_re
+    ) {
+      my $data = $bound->[1];
 
-        my @parts = unpack '(a2000)*', $data;
+      $data = "$data" if ref $data;
 
-        my @sql_frag;
+      my @parts = unpack '(a2000)*', $data;
 
-        for my $idx (0..$#parts) {
-          push @sql_frag, sprintf (
-            'UTL_RAW.CAST_TO_VARCHAR2(RAWTOHEX(DBMS_LOB.SUBSTR(%s, 2000, %d))) = ?',
-            $col, ($idx*2000 + 1),
-          );
-        }
+      my @sql_frag;
 
-        my $sql_frag = '( ' . (join ' AND ', @sql_frag) . ' )';
+      for my $idx (0..$#parts) {
+        push @sql_frag, sprintf (
+          'UTL_RAW.CAST_TO_VARCHAR2(RAWTOHEX(DBMS_LOB.SUBSTR(%s, 2000, %d))) = ?',
+          $col, ($idx*2000 + 1),
+        );
+      }
 
-        $sql_parts[0] =~ s/$col_equality_re/$sql_frag/;
+      my $sql_frag = '( ' . (join ' AND ', @sql_frag) . ' )';
 
-        $new_sql .= shift @sql_parts;
+      $sql_parts[0] =~ s/$col_equality_re/$sql_frag/;
 
-        for my $idx (0..$#parts) {
-          push @new_binds, [
-            {
-              %{ $bound->[0] },
-              _ora_lob_autosplit_part => $idx,
-              dbd_attrs => undef,
-            },
-            $parts[$idx]
-          ];
-        }
-      }
-      else {
-        $new_sql .= shift(@sql_parts) . '?';
+      $final_sql .= shift @sql_parts;
 
-        push @new_binds, [
+      for my $idx (0..$#parts) {
+        push @final_binds, [
           {
             %{ $bound->[0] },
+            _ora_lob_autosplit_part => $idx,
             dbd_attrs => undef,
           },
-          $bound->[1],
+          $parts[$idx]
         ];
       }
     }
     else {
-      $new_sql .= shift(@sql_parts) . '?';
-      push @new_binds, $bound;
+      $final_sql .= shift(@sql_parts) . '?';
+      push @final_binds, $lob_bind_indices->{$b_idx}
+        ? [
+          {
+            %{ $bound->[0] },
+            dbd_attrs => undef,
+          },
+          $bound->[1],
+        ] : $bound
+      ;
     }
   }
 
@@ -591,9 +598,9 @@ sub _prep_for_execute {
     @sql_parts = join ('?', @sql_parts);
   }
 
-  $new_sql .= $sql_parts[0];
+  $final_sql .= $sql_parts[0];
 
-  return ($new_sql, \@new_binds);
+  return ($final_sql, \@final_binds);
 }
 
 # Savepoints stuff.
index aafd1f0..6e1026e 100644 (file)
@@ -393,7 +393,6 @@ sub _run_tests {
 
     # disable BLOB mega-output
     my $orig_debug = $schema->storage->debug;
-    $schema->storage->debug (0);
 
     local $TODO = 'Something is confusing column bindtype assignment when quotes are active'
                 . ': https://rt.cpan.org/Ticket/Display.html?id=64206'
@@ -403,6 +402,13 @@ sub _run_tests {
     foreach my $size (qw( small large )) {
       $id++;
 
+      if ($size eq 'small') {
+        $schema->storage->debug($orig_debug);
+      }
+      elsif ($size eq 'large') {
+        $schema->storage->debug(0);
+      }
+
       my $str = $binstr{$size};
       lives_ok {
         $rs->create( { 'id' => $id, blob => "blob:$str", clob => "clob:$str" } )
@@ -441,7 +447,7 @@ sub _run_tests {
       is (@objs, 1, 'One row found matching on both LOBs as a subquery');
 
       lives_ok {
-        $rs->search({ blob => "blob:$str", clob => "clob:$str" })
+        $rs->search({ id => $id, blob => "blob:$str", clob => "clob:$str" })
           ->update({ blob => 'updated blob', clob => 'updated clob' });
       } 'blob UPDATE with WHERE clause survived';