Make Access inner joins 'INNER JOIN' to avoid JOIN syntax errors
[dbsrgits/DBIx-Class.git] / lib / DBIx / Class / Storage / DBI / Oracle / Generic.pm
index 8e769b6..12a14d3 100644 (file)
@@ -2,13 +2,21 @@ package DBIx::Class::Storage::DBI::Oracle::Generic;
 
 use strict;
 use warnings;
+use base qw/DBIx::Class::Storage::DBI/;
+use mro 'c3';
+use DBIx::Class::Carp;
 use Scope::Guard ();
 use Context::Preserve 'preserve_context';
 use Try::Tiny;
+use List::Util 'first';
 use namespace::clean;
 
 __PACKAGE__->sql_limit_dialect ('RowNum');
 __PACKAGE__->sql_quote_char ('"');
+__PACKAGE__->sql_maker_class('DBIx::Class::SQLMaker::Oracle');
+__PACKAGE__->datetime_parser_type('DateTime::Format::Oracle');
+
+sub __cache_queries_with_max_lob_parts { 2 }
 
 =head1 NAME
 
@@ -70,18 +78,12 @@ DBIx::Class::Storage::DBI::Oracle::Generic - Oracle Support for DBIx::Class
 
 This class implements base Oracle support. The subclass
 L<DBIx::Class::Storage::DBI::Oracle::WhereJoins> is for C<(+)> joins in Oracle
-versions before 9.
+versions before 9.0.
 
 =head1 METHODS
 
 =cut
 
-use base qw/DBIx::Class::Storage::DBI/;
-use mro 'c3';
-
-__PACKAGE__->sql_maker_class('DBIx::Class::SQLMaker::Oracle');
-__PACKAGE__->datetime_parser_type('DateTime::Format::Oracle');
-
 sub _determine_supports_insert_returning {
   my $self = shift;
 
@@ -156,7 +158,7 @@ sub _dbh_get_autoinc_seq {
   my ( $schema, $table ) = $source_name =~ /( (?:${ql})? \w+ (?:${qr})? ) \. ( (?:${ql})? \w+ (?:${qr})? )/x;
 
   # if no explicit schema was requested - use the default schema (which in the case of Oracle is the db user)
-  $schema ||= uc( ($self->_dbi_connect_info||[])->[1] || '');
+  $schema ||= \'= USER';
 
   my ($sql, @bind) = $sql_maker->select (
     'ALL_TRIGGERS',
@@ -272,15 +274,23 @@ sub _ping {
 }
 
 sub _dbh_execute {
-  my $self = shift;
-  my ($dbh, $op, $extra_bind, $ident, $bind_attributes, @args) = @_;
+  my ($self, $dbh, $sql, @args) = @_;
 
   my (@res, $tried);
   my $want = wantarray;
   my $next = $self->next::can;
   do {
     try {
-      my $exec = sub { $self->$next($dbh, $op, $extra_bind, $ident, $bind_attributes, @args) };
+      my $exec = sub {
+        # Turn off sth caching for multi-part LOBs. See _prep_for_execute above.
+        local $self->{disable_sth_caching} = 1
+          if first {
+            ($_->[0]{_ora_lob_autosplit_part}||0)
+              > (__cache_queries_with_max_lob_parts-1)
+          } @{ $args[0] };
+
+        $self->$next($dbh, $sql, @args)
+      };
 
       if (!defined $want) {
         $exec->();
@@ -298,7 +308,6 @@ sub _dbh_execute {
       if (! $tried and $_ =~ /ORA-01003/) {
         # ORA-01003: no statement parsed (someone changed the table somehow,
         # invalidating your cursor.)
-        my ($sql, $bind) = $self->_prep_for_execute($op, $extra_bind, $ident, \@args);
         delete $dbh->{CachedKids}{$sql};
       }
       else {
@@ -384,69 +393,213 @@ sub connect_call_datetime_setup {
   );
 }
 
-=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.
+### Note originally by Ron "Quinn" Straight <quinnfazigu@gmail.org>
+### http://git.shadowcat.co.uk/gitweb/gitweb.cgi?p=dbsrgits/DBIx-Class.git;a=commitdiff;h=5db2758de644d53e07cd3e05f0e9037bf40116fc
+#
+# 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.
+#
+sub _dbi_attrs_for_bind {
+  my ($self, $ident, $bind) = @_;
+
+  my $attrs = $self->next::method($ident, $bind);
+
+  for my $i (0 .. $#$attrs) {
+    if (keys %{$attrs->[$i]||{}} and my $col = $bind->[$i][0]{dbic_colname}) {
+      $attrs->[$i]{ora_field} = $col;
+    }
+  }
 
-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.
+  $attrs;
+}
 
-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.
+my $dbd_loaded;
+sub bind_attribute_by_data_type {
+  my ($self, $dt) = @_;
+
+  $dbd_loaded ||= do {
+    require DBD::Oracle;
+    if ($DBD::Oracle::VERSION eq '1.23') {
+      $self->throw_exception(
+        "BLOB/CLOB support in DBD::Oracle == 1.23 is broken, use an earlier or later ".
+        "version.\n\nSee: https://rt.cpan.org/Public/Bug/Display.html?id=46016\n"
+      );
+    }
+    1;
+  };
 
-=cut
+  if ($self->_is_lob_type($dt)) {
+    return {
+      ora_type => $self->_is_text_lob_type($dt)
+        ? DBD::Oracle::ORA_CLOB()
+        : DBD::Oracle::ORA_BLOB()
+    };
+  }
+}
 
-sub source_bind_attributes
-{
-  require DBD::Oracle;
+# Handle blob columns in WHERE.
+#
+# For equality comparisons:
+#
+# We split data intended for comparing to a LOB into 2000 character chunks and
+# compare them using dbms_lob.substr on the LOB column.
+#
+# We turn off DBD::Oracle LOB binds for these partial LOB comparisons by passing
+# dbd_attrs => undef, because these are regular varchar2 comparisons and
+# otherwise the query will fail.
+#
+# Since the most common comparison size is likely to be under 4000 characters
+# (TEXT comparisons previously deployed to other RDBMSes) we disable
+# prepare_cached for queries with more than two part comparisons to a LOB
+# column. This is done in _dbh_execute (above) which was previously overridden
+# to gracefully recover from an Oracle error. This is to be careful to not
+# exhaust your application's open cursor limit.
+#
+# See:
+# http://itcareershift.com/blog1/2011/02/21/oracle-max-number-of-open-cursors-complete-reference-for-the-new-oracle-dba/
+# on the open_cursor limit.
+#
+# For everything else:
+#
+# We assume that everything that is not a LOB comparison, will most likely be a
+# LIKE query or some sort of function invocation. This may prove to be a naive
+# assumption in the future, but for now it should cover the two most likely
+# things users would want to do with a BLOB or CLOB, an equality test or a LIKE
+# query (on a CLOB.)
+#
+# For these expressions, the bind must NOT have the attributes of a LOB bind for
+# DBD::Oracle, otherwise the query will fail. This is done by passing
+# dbd_attrs => undef.
+
+sub _prep_for_execute {
   my $self = shift;
-  my($source) = @_;
+  my ($op) = @_;
 
-  my %bind_attributes = %{ $self->next::method(@_) };
+  my ($sql, $bind) = $self->next::method(@_);
 
-  foreach my $column ($source->columns) {
-    my %column_bind_attrs = %{ $bind_attributes{$column} || {} };
+  return ($sql, $bind) if $op eq 'insert';
 
-    my $data_type = $source->column_info($column)->{data_type};
+  my $blob_bind_index;
+  for (0 .. $#$bind) {
+    $blob_bind_index->{$_} = 1 if $self->_is_lob_type(
+      $bind->[$_][0]{sqlt_datatype}
+    );
+  }
 
-    if ($self->_is_lob_type($data_type)) {
-      if ($DBD::Oracle::VERSION eq '1.23') {
-        $self->throw_exception(
-"BLOB/CLOB support in DBD::Oracle == 1.23 is broken, use an earlier or later ".
-"version.\n\nSee: https://rt.cpan.org/Public/Bug/Display.html?id=46016\n"
-        );
-      }
+  return ($sql, $bind) unless $blob_bind_index;
 
-      $column_bind_attrs{'ora_type'} = $self->_is_text_lob_type($data_type)
-        ? DBD::Oracle::ORA_CLOB()
-        : DBD::Oracle::ORA_BLOB()
-      ;
-      $column_bind_attrs{'ora_field'} = $column;
+  my (@sql_parts, $new_sql, @new_binds);
+
+  if ($op eq 'select' || $op eq 'delete') {
+    @sql_parts = split /\?/, $sql;
+  }
+  elsif ($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;
+
+    my $set_bind_count = $set_part =~ y/?//;
+    @new_binds = splice @$bind, 0, $set_bind_count;
+
+    @sql_parts = split /\?/, $where_part;
+    $new_sql  = $set_part;
+  }
+  else {
+    $self->throw_exception("Unsupported \$op: $op");
+  }
+
+  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;
+
+        my @parts = unpack '(a2000)*', $data;
+
+        my @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),
+          );
+        }
+
+        my $sql_frag = '( ' . (join ' AND ', @sql_frag) . ' )';
+
+        $sql_parts[0] =~ s/$col_equality_re/$sql_frag/;
+
+        $new_sql .= shift @sql_parts;
+
+        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) . '?';
+
+        push @new_binds, [
+          {
+            %{ $bound->[0] },
+            dbd_attrs => undef,
+          },
+          $bound->[1],
+        ];
+      }
+    }
+    else {
+      $new_sql .= shift(@sql_parts) . '?';
+      push @new_binds, $bound;
     }
+  }
 
-    $bind_attributes{$column} = \%column_bind_attrs;
+  if (@sql_parts > 1) {
+    carp "There are more placeholders than binds, this should not happen!";
+    @sql_parts = join ('?', @sql_parts);
   }
 
-  return \%bind_attributes;
+  $new_sql .= $sql_parts[0];
+
+  return ($new_sql, \@new_binds);
 }
 
-sub _svp_begin {
+# Savepoints stuff.
+
+sub _exec_svp_begin {
   my ($self, $name) = @_;
-  $self->_get_dbh->do("SAVEPOINT $name");
+  $self->_dbh->do("SAVEPOINT $name");
 }
 
 # Oracle automatically releases a savepoint when you start another one with the
 # same name.
-sub _svp_release { 1 }
+sub _exec_svp_release { 1 }
 
-sub _svp_rollback {
+sub _exec_svp_rollback {
   my ($self, $name) = @_;
-  $self->_get_dbh->do("ROLLBACK TO SAVEPOINT $name")
+  $self->_dbh->do("ROLLBACK TO SAVEPOINT $name")
 }
 
 =head2 relname_to_table_alias
@@ -588,7 +741,7 @@ It uses the same syntax as L<DBIx::Class::ResultSet/order_by>
 
 =head1 AUTHOR
 
-See L<DBIx::Class/CONTRIBUTORS>.
+See L<DBIx::Class/AUTHOR> and L<DBIx::Class/CONTRIBUTORS>.
 
 =head1 LICENSE
 
@@ -597,3 +750,4 @@ You may distribute this code under the same terms as Perl itself.
 =cut
 
 1;
+# vim:sts=2 sw=2: