Properly support ops containing _'s (valid in Oracle)
[dbsrgits/SQL-Abstract.git] / lib / SQL / Abstract.pm
index 6212ae9..f646790 100644 (file)
@@ -15,7 +15,7 @@ use Scalar::Util ();
 # GLOBALS
 #======================================================================
 
-our $VERSION  = '1.67';
+our $VERSION  = '1.68';
 
 # This would confuse some packagers
 $VERSION = eval $VERSION if $VERSION =~ /_/; # numify for warning-free dev releases
@@ -25,17 +25,17 @@ our $AUTOLOAD;
 # special operators (-in, -between). May be extended/overridden by user.
 # See section WHERE: BUILTIN SPECIAL OPERATORS below for implementation
 my @BUILTIN_SPECIAL_OPS = (
-  {regex => qr/^(not )?between$/i, handler => '_where_field_BETWEEN'},
-  {regex => qr/^(not )?in$/i,      handler => '_where_field_IN'},
+  {regex => qr/^ (?: not \s )? between $/ix, handler => '_where_field_BETWEEN'},
+  {regex => qr/^ (?: not \s )? in      $/ix, handler => '_where_field_IN'},
 );
 
 # unaryish operators - key maps to handler
 my @BUILTIN_UNARY_OPS = (
   # the digits are backcompat stuff
-  { regex => qr/^and  (?: \s? \d+ )? $/xi, handler => '_where_op_ANDOR' },
-  { regex => qr/^or   (?: \s? \d+ )? $/xi, handler => '_where_op_ANDOR' },
-  { regex => qr/^nest (?: \s? \d+ )? $/xi, handler => '_where_op_NEST' },
-  { regex => qr/^ (?: not \s )? bool $/xi, handler => '_where_op_BOOL' },
+  { regex => qr/^ and  (?: [_\s]? \d+ )? $/xi, handler => '_where_op_ANDOR' },
+  { regex => qr/^ or   (?: [_\s]? \d+ )? $/xi, handler => '_where_op_ANDOR' },
+  { regex => qr/^ nest (?: [_\s]? \d+ )? $/xi, handler => '_where_op_NEST' },
+  { regex => qr/^ (?: not \s )? bool     $/xi, handler => '_where_op_BOOL' },
 );
 
 #======================================================================
@@ -463,16 +463,19 @@ sub _where_HASHREF {
       if ($k =~ /^-./) {
         # put the operator in canonical form
         my $op = $k;
-        $op =~ s/^-//;        # remove initial dash
-        $op =~ s/[_\t ]+/ /g; # underscores and whitespace become single spaces
+        $op = substr $op, 1;  # remove initial dash
         $op =~ s/^\s+|\s+$//g;# remove leading/trailing space
+        $op =~ s/\s+/ /g;     # compress whitespace
+
+        # so that -not_foo works correctly
+        $op =~ s/^not_/NOT /i;
 
         $self->_debug("Unary OP(-$op) within hashref, recursing...");
 
         my $op_entry = List::Util::first {$op =~ $_->{regex}} @{$self->{unary_ops}};
         if (my $handler = $op_entry->{handler}) {
           if (not ref $handler) {
-            if ($op =~ s/\s?\d+$//) {
+            if ($op =~ s/ [_\s]? \d+ $//x ) {
               belch 'Use of [and|or|nest]_N modifiers is deprecated and will be removed in SQLA v2.0. '
                   . "You probably wanted ...-and => [ -$op => COND1, -$op => COND2 ... ]";
               }
@@ -487,9 +490,9 @@ sub _where_HASHREF {
         }
         else {
           $self->debug("Generic unary OP: $k - recursing as function");
-          my ($sql, @bind) = $self->_where_func_generic ($op, $v);
-          $sql = "($sql)" unless (defined($self->{_nested_func_lhs}) && ($self->{_nested_func_lhs} eq $k));  # top level vs nested
-          ($sql, @bind);
+          my ($s, @b) = $self->_where_func_generic ($op, $v);
+          $s = "($s)" unless (defined($self->{_nested_func_lhs}) && ($self->{_nested_func_lhs} eq $k));  # top level vs nested
+          ($s, @b);
         }
       }
       else {
@@ -589,30 +592,22 @@ sub _where_op_NEST {
 sub _where_op_BOOL {
   my ($self, $op, $v) = @_;
 
-  my ( $prefix, $suffix ) = ( $op =~ /\bnot\b/i )
-    ? ( '(NOT ', ')' )
-    : ( '', '' );
-
-  my ($sql, @bind) = do {
-    $self->_SWITCH_refkind($v, {
-      SCALAR => sub { # interpreted as SQL column
-        $self->_convert($self->_quote($v));
-      },
+  my ($s, @b) = $self->_SWITCH_refkind($v, {
+    SCALAR => sub { # interpreted as SQL column
+      $self->_convert($self->_quote($v));
+    },
 
-      UNDEF => sub {
-        puke "-$op => undef not supported";
-      },
+    UNDEF => sub {
+      puke "-$op => undef not supported";
+    },
 
-      FALLBACK => sub {
-        $self->_recurse_where ($v);
-      },
-    });
-  };
+    FALLBACK => sub {
+      $self->_recurse_where ($v);
+    },
+  });
 
-  return (
-    join ('', $prefix, $sql, $suffix),
-    @bind,
-  );
+  $s = "(NOT $s)" if $op =~ /^not/i;
+  ($s, @b);
 }
 
 
@@ -660,9 +655,14 @@ sub _where_hashpair_HASHREF {
 
     # put the operator in canonical form
     my $op = $orig_op;
-    $op =~ s/^-//;        # remove initial dash
-    $op =~ s/[_\t ]+/ /g; # underscores and whitespace become single spaces
+
+    # FIXME - we need to phase out dash-less ops
+    $op =~ s/^-//;        # remove possible initial dash
     $op =~ s/^\s+|\s+$//g;# remove leading/trailing space
+    $op =~ s/\s+/ /g;     # compress whitespace
+
+    # so that -not_foo works correctly
+    $op =~ s/^not_/NOT /i;
 
     my ($sql, @bind);
 
@@ -871,16 +871,23 @@ sub _where_field_BETWEEN {
       foreach my $val (@$vals) {
         my ($sql, @bind) = $self->_SWITCH_refkind($val, {
            SCALAR => sub {
-             return ($placeholder, ($val));
+             return ($placeholder, $val);
            },
            SCALARREF => sub {
-             return ($self->_convert($$val), ());
+             return $$val;
            },
            ARRAYREFREF => sub {
              my ($sql, @bind) = @$$val;
              $self->_assert_bindval_matches_bindtype(@bind);
-             return ($self->_convert($sql), @bind);
+             return ($sql, @bind);
            },
+           HASHREF => sub {
+             my ($func, $arg, @rest) = %$val;
+             puke ("Only simple { -func => arg } functions accepted as sub-arguments to BETWEEN")
+               if (@rest or $func !~ /^ \- (.+)/x);
+             local $self->{_nested_func_lhs} = $k;
+             $self->_where_func_generic ($1 => $arg);
+           }
         });
         push @all_sql, $sql;
         push @all_bind, @bind;
@@ -914,11 +921,41 @@ sub _where_field_IN {
   my ($sql, @bind) = $self->_SWITCH_refkind($vals, {
     ARRAYREF => sub {     # list of choices
       if (@$vals) { # nonempty list
-        my $placeholders  = join ", ", (($placeholder) x @$vals);
-        my $sql           = "$label $op ( $placeholders )";
-        my @bind = $self->_bindtype($k, @$vals);
+        my (@all_sql, @all_bind);
+
+        for my $val (@$vals) {
+          my ($sql, @bind) = $self->_SWITCH_refkind($val, {
+            SCALAR => sub {
+              return ($placeholder, $val);
+            },
+            SCALARREF => sub {
+              return $$val;
+            },
+            ARRAYREFREF => sub {
+              my ($sql, @bind) = @$$val;
+              $self->_assert_bindval_matches_bindtype(@bind);
+              return ($sql, @bind);
+            },
+            HASHREF => sub {
+              my ($func, $arg, @rest) = %$val;
+              puke ("Only simple { -func => arg } functions accepted as sub-arguments to IN")
+                if (@rest or $func !~ /^ \- (.+)/x);
+              local $self->{_nested_func_lhs} = $k;
+              $self->_where_func_generic ($1 => $arg);
+            }
+          });
+          push @all_sql, $sql;
+          push @all_bind, @bind;
+        }
 
-        return ($sql, @bind);
+        return (
+          sprintf ('%s %s ( %s )',
+            $label,
+            $op,
+            join (', ', @all_sql)
+          ),
+          $self->_bindtype($k, @all_bind),
+        );
       }
       else { # empty list : some databases won't understand "IN ()", so DWIM
         my $sql = ($op =~ /\bnot\b/i) ? $self->{sqltrue} : $self->{sqlfalse};
@@ -1900,6 +1937,20 @@ This simple code will create the following:
 A field associated to an empty arrayref will be considered a
 logical false and will generate 0=1.
 
+=head2 Tests for NULL values
+
+If the value part is C<undef> then this is converted to SQL <IS NULL>
+
+    my %where  = (
+        user   => 'nwiger',
+        status => undef,
+    );
+
+becomes:
+
+    $stmt = "WHERE user = ? AND status IS NULL";
+    @bind = ('nwiger');
+
 =head2 Specific comparison operators
 
 If you want to specify a different type of operator for your comparison,
@@ -2098,7 +2149,7 @@ list can be expanded : see section L</"SPECIAL OPERATORS"> below.
 If you wish to test against boolean columns or functions within your
 database you can use the C<-bool> and C<-not_bool> operators. For
 example to test the column C<is_user> being true and the column
-<is_enabled> being false you would use:-
+C<is_enabled> being false you would use:-
 
     my %where  = (
         -bool       => 'is_user',
@@ -2261,6 +2312,17 @@ which yields
     $stmt = "WHERE priority < ? AND is_ready";
     @bind = ('2');
 
+Literal SQL is also the only way to compare 2 columns to one another:
+
+    my %where = (
+        priority => { '<', 2 },
+        requestor => \'= submittor'
+    );
+
+which creates:
+
+    $stmt = "WHERE priority < ? AND requestor = submitter";
+    @bind = ('2');
 
 =head2 Literal SQL with placeholders and bind values (subqueries)
 
@@ -2584,6 +2646,12 @@ the same structure, you only have to generate the SQL the first time
 around. On subsequent queries, simply use the C<values> function provided
 by this module to return your values in the correct order.
 
+However this depends on the values having the same type - if, for
+example, the values of a where clause may either have values
+(resulting in sql of the form C<column = ?> with a single bind
+value), or alternatively the values might be C<undef> (resulting in
+sql of the form C<column IS NULL> with no bind value) then the
+caching technique suggested will not work.
 
 =head1 FORMBUILDER