Merge 'trunk' into 'bool_operator'
Peter Rabbitson [Thu, 20 Aug 2009 08:17:30 +0000 (08:17 +0000)]
r6291@Thesaurus (orig r6290):  ribasushi | 2009-05-17 00:45:12 +0200
Test and fix for obscure where-cond modification
r6292@Thesaurus (orig r6291):  ribasushi | 2009-05-17 01:25:10 +0200
Release 1.55
r6453@Thesaurus (orig r6452):  mo | 2009-05-29 15:41:22 +0200
added failing test for -desc => \['colA LIKE ?', 'test']
r6454@Thesaurus (orig r6453):  ribasushi | 2009-05-29 17:41:10 +0200
Fix for _order_by with bind values - will not work on DBIC - needs matching changes to SQLAHacks
r6455@Thesaurus (orig r6454):  mo | 2009-05-29 18:28:54 +0200
order_by: added passing test
r6461@Thesaurus (orig r6460):  ribasushi | 2009-05-30 10:10:38 +0200
Do not join hash order conditions early
r6466@Thesaurus (orig r6465):  ribasushi | 2009-05-30 18:35:46 +0200
Release 1.56

1  2 
lib/SQL/Abstract.pm

diff --combined lib/SQL/Abstract.pm
@@@ -15,7 -15,7 +15,7 @@@ use Scalar::Util qw/blessed/
  # GLOBALS
  #======================================================================
  
- our $VERSION  = '1.54';
+ our $VERSION  = '1.56';
  
  # This would confuse some packagers
  #$VERSION      = eval $VERSION; # numify for warning-free dev releases
@@@ -29,14 -29,6 +29,14 @@@ my @BUILTIN_SPECIAL_OPS = 
    {regex => qr/^(not )?in$/i,      handler => '_where_field_IN'},
  );
  
 +# unaryish operators - key maps to handler
 +my @BUILTIN_UNARY_OPS = (
 +  { 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' },
 +);
 +
  #======================================================================
  # DEBUGGING AND ERROR REPORTING
  #======================================================================
@@@ -94,10 -86,6 +94,10 @@@ sub new 
    $opt{special_ops} ||= [];
    push @{$opt{special_ops}}, @BUILTIN_SPECIAL_OPS;
  
 +  # unary operators 
 +  $opt{unary_ops} ||= [];
 +  push @{$opt{unary_ops}}, @BUILTIN_UNARY_OPS;
 +
    return bless \%opt, $class;
  }
  
@@@ -453,98 -441,49 +453,98 @@@ sub _where_HASHREF 
  
  
  sub _where_op_in_hash {
 -  my ($self, $op_str, $v) = @_; 
 +  my ($self, $op, $v) = @_; 
 +
 +  # put the operator in canonical form
 +  $op =~ s/^-//;       # remove initial dash
 +  $op =~ tr/_/ /;      # underscores become spaces
 +  $op =~ s/^\s+//;     # no initial space
 +  $op =~ s/\s+$//;     # no final space
 +  $op =~ s/\s+/ /;     # multiple spaces become one
 +
 +  $self->_debug("OP(-$op) within hashref, recursing...");
 +
 +  my $op_entry = first {$op =~ $_->{regex}} @{$self->{unary_ops}};
 +  my $handler = $op_entry->{handler};
 +  if (! $handler) {
 +    puke "unknown operator: -$op";
 +  }
 +  elsif (not ref $handler) {
 +    return $self->$handler ($op, $v);
 +  }
 +  elsif (ref $handler eq 'CODE') {
 +    return $handler->($self, $op, $v);
 +  }
 +  else {
 +    puke "Illegal handler for operator $op - expecting a method name or a coderef";
 +  }
 +}
  
 -  $op_str =~ /^ (AND|OR|NEST) ( \_? \d* ) $/xi
 -    or puke "unknown operator: -$op_str";
 +sub _where_op_ANDOR {
 +  my ($self, $op, $v) = @_; 
  
 -  my $op = uc($1); # uppercase, remove trailing digits
 -  if ($2) {
 +  if ($op =~ s/\s?\d+$//) {
      belch 'Use of [and|or|nest]_N modifiers is deprecated and will be removed in SQLA v2.0. '
 -          . "You probably wanted ...-and => [ $op_str => COND1, $op_str => COND2 ... ]";
 +          . "You probably wanted ...-and => [ $op => COND1, $op => COND2 ... ]";
 +  }
 +
 +  $self->_SWITCH_refkind($v, {
 +    ARRAYREF => sub {
 +      return $self->_where_ARRAYREF($v, $op);
 +    },
 +
 +    HASHREF => sub {
 +      return ( $op =~ /^or/i )
 +        ? $self->_where_ARRAYREF( [ map { $_ => $v->{$_} } ( sort keys %$v ) ], $op )
 +        : $self->_where_HASHREF($v);
 +    },
 +
 +    SCALARREF  => sub { 
 +      puke "-$op => \\\$scalar not supported, use -nest => ...";
 +    },
 +
 +    ARRAYREFREF => sub {
 +      puke "-$op => \\[..] not supported, use -nest => ...";
 +    },
 +
 +    SCALAR => sub { # permissively interpreted as SQL
 +      puke "-$op => 'scalar' not supported, use -nest => \\'scalar'";
 +    },
 +
 +    UNDEF => sub {
 +      puke "-$op => undef not supported";
 +    },
 +   });
 +}
 +
 +sub _where_op_NEST {
 +  my ($self, $op, $v) = @_; 
 +
 +  if ($op =~ s/\s?\d+$//) {
 +    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 ... ]";
    }
  
 -  $self->_debug("OP(-$op) within hashref, recursing...");
  
    $self->_SWITCH_refkind($v, {
  
      ARRAYREF => sub {
 -      return $self->_where_ARRAYREF($v, $op eq 'NEST' ? '' : $op);
 +      return $self->_where_ARRAYREF($v, '');
      },
  
      HASHREF => sub {
 -      if ($op eq 'OR') {
 -        return $self->_where_ARRAYREF([ map { $_ => $v->{$_} } (sort keys %$v) ], 'OR');
 -      } 
 -      else {                  # NEST | AND
 -        return $self->_where_HASHREF($v);
 -      }
 +      return $self->_where_HASHREF($v);
      },
  
      SCALARREF  => sub {         # literal SQL
 -      $op eq 'NEST' 
 -        or puke "-$op => \\\$scalar not supported, use -nest => ...";
        return ($$v); 
      },
  
      ARRAYREFREF => sub {        # literal SQL
 -      $op eq 'NEST' 
 -        or puke "-$op => \\[..] not supported, use -nest => ...";
        return @{${$v}};
      },
  
      SCALAR => sub { # permissively interpreted as SQL
 -      $op eq 'NEST' 
 -        or puke "-$op => 'scalar' not supported, use -nest => \\'scalar'";
        belch "literal SQL should be -nest => \\'scalar' "
            . "instead of -nest => 'scalar' ";
        return ($v); 
  }
  
  
 +sub _where_op_BOOL {
 +  my ($self, $op, $v) = @_; 
 +
 +  my $prefix = ($op =~ /\bnot\b/i) ? 'NOT ' : '';
 +  $self->_SWITCH_refkind($v, {
 +    SCALARREF  => sub {         # literal SQL
 +      return ($prefix . $$v); 
 +    },
 +
 +    SCALAR => sub { # interpreted as SQL column
 +      return ($prefix . $self->_convert($self->_quote($v))); 
 +    },
 +   });
 +}
 +
 +
  sub _where_hashpair_ARRAYREF {
    my ($self, $k, $v) = @_;
  
@@@ -692,18 -615,20 +692,20 @@@ sub _where_hashpair_HASHREF 
  sub _where_field_op_ARRAYREF {
    my ($self, $k, $op, $vals) = @_;
  
-   if(@$vals) {
-     $self->_debug("ARRAY($vals) means multiple elements: [ @$vals ]");
+   my @vals = @$vals;  #always work on a copy
+   if(@vals) {
+     $self->_debug("ARRAY($vals) means multiple elements: [ @vals ]");
  
      # see if the first element is an -and/-or op
      my $logic;
-     if ($vals->[0] =~ /^ - ( AND|OR ) $/ix) {
+     if ($vals[0] =~ /^ - ( AND|OR ) $/ix) {
        $logic = uc $1;
-       shift @$vals;
+       shift @vals;
      }
  
-     # distribute $op over each remaining member of @$vals, append logic if exists
-     return $self->_recurse_where([map { {$k => {$op, $_}} } @$vals], $logic);
+     # distribute $op over each remaining member of @vals, append logic if exists
+     return $self->_recurse_where([map { {$k => {$op, $_}} } @vals], $logic);
  
      # LDNOTE : had planned to change the distribution logic when 
      # $op =~ $self->{inequality_op}, because of Morgan laws : 
      # WHERE field != 22 AND field != 33.
      # To do this, replace the above to roughly :
      # my $logic = ($op =~ $self->{inequality_op}) ? 'AND' : 'OR';
-     # return $self->_recurse_where([map { {$k => {$op, $_}} } @$vals], $logic);
+     # return $self->_recurse_where([map { {$k => {$op, $_}} } @vals], $logic);
  
    } 
    else {
@@@ -879,6 -804,8 +881,6 @@@ sub _where_field_IN 
  
  
  
 -
 -
  #======================================================================
  # ORDER BY
  #======================================================================
  sub _order_by {
    my ($self, $arg) = @_;
  
-   # construct list of ordering instructions
-   my @order = $self->_SWITCH_refkind($arg, {
+   my (@sql, @bind);
+   for my $c ($self->_order_by_chunks ($arg) ) {
+     $self->_SWITCH_refkind ($c, {
+       SCALAR => sub { push @sql, $c },
+       ARRAYREF => sub { push @sql, shift @$c; push @bind, @$c },
+     });
+   }
+   my $sql = @sql
+     ? sprintf ('%s %s',
+         $self->_sqlcase(' order by'),
+         join (', ', @sql)
+       )
+     : ''
+   ;
+   return wantarray ? ($sql, @bind) : $sql;
+ }
+ sub _order_by_chunks {
+   my ($self, $arg) = @_;
+   return $self->_SWITCH_refkind($arg, {
  
      ARRAYREF => sub {
-       map {$self->_SWITCH_refkind($_, {
-               SCALAR    => sub {$self->_quote($_)},
-               UNDEF     => sub {},
-               SCALARREF => sub {$$_}, # literal SQL, no quoting
-               HASHREF   => sub {$self->_order_by_hash($_)}
-              }) } @$arg;
+       map { $self->_order_by_chunks ($_ ) } @$arg;
      },
  
+     ARRAYREFREF => sub { [ @$$arg ] },
      SCALAR    => sub {$self->_quote($arg)},
-     UNDEF     => sub {},
+     UNDEF     => sub {return () },
      SCALARREF => sub {$$arg}, # literal SQL, no quoting
-     HASHREF   => sub {$self->_order_by_hash($arg)},
  
-   });
+     HASHREF   => sub {
+       # get first pair in hash
+       my ($key, $val) = each %$arg;
  
-   # build SQL
-   my $order = join ', ', @order;
-   return $order ? $self->_sqlcase(' order by')." $order" : '';
- }
+       return () unless $key;
+       if ( (keys %$arg) > 1 or not $key =~ /^-(desc|asc)/i ) {
+         puke "hash passed to _order_by must have exactly one key (-desc or -asc)";
+       }
  
+       my $direction = $1;
  
- sub _order_by_hash {
-   my ($self, $hash) = @_;
+       my @ret;
+       for my $c ($self->_order_by_chunks ($val)) {
+         my ($sql, @bind);
  
-   # get first pair in hash
-   my ($key, $val) = each %$hash;
+         $self->_SWITCH_refkind ($c, {
+           SCALAR => sub {
+             $sql = $c;
+           },
+           ARRAYREF => sub {
+             ($sql, @bind) = @$c;
+           },
+         });
  
-   # check if one pair was found and no other pair in hash
-   $key && !(each %$hash)
-     or puke "hash passed to _order_by must have exactly one key (-desc or -asc)";
+         $sql = $sql . ' ' . $self->_sqlcase($direction);
  
-   my ($order) = ($key =~ /^-(desc|asc)/i)
-     or puke "invalid key in _order_by hash : $key";
+         push @ret, [ $sql, @bind];
+       }
  
-   $val = ref $val eq 'ARRAY' ? $val : [$val];
-   return join ', ', map { $self->_quote($_) . ' ' . $self->_sqlcase($order) } @$val;
+       return @ret;
+     },
+   });
  }
  
  
  #======================================================================
  # DATASOURCE (FOR NOW, JUST PLAIN TABLE OR LIST OF TABLES)
  #======================================================================
@@@ -1594,12 -1549,6 +1624,12 @@@ Takes a reference to a list of "specia
  to extend the syntax understood by L<SQL::Abstract>.
  See section L</"SPECIAL OPERATORS"> for details.
  
 +=item unary_ops
 +
 +Takes a reference to a list of "unary operators" 
 +to extend the syntax understood by L<SQL::Abstract>.
 +See section L</"UNARY OPERATORS"> for details.
 +
  
  
  =back
@@@ -1920,24 -1869,6 +1950,24 @@@ Would give you
  These are the two builtin "special operators"; but the 
  list can be expanded : see section L</"SPECIAL OPERATORS"> below.
  
 +=head2 Unary operators: bool
 +
 +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:-
 +
 +    my %where  = (
 +        -bool       => 'is_user',
 +        -not_bool   => 'is_enabled',
 +    );
 +
 +Would give you:
 +
 +    WHERE is_user AND NOT is_enabledmv 
 +
 +
 +
  =head2 Nested conditions, -and/-or prefixes
  
  So far, we've seen how multiple conditions are joined with a top-level
@@@ -2316,59 -2247,6 +2346,59 @@@ of the MATCH .. AGAINST syntax for MySQ
    ]);
  
  
 +=head1 UNARY OPERATORS
 +
 +  my $sqlmaker = SQL::Abstract->new(unary_ops => [
 +     {
 +      regex => qr/.../,
 +      handler => sub {
 +        my ($self, $op, $arg) = @_;
 +        ...
 +      },
 +     },
 +     {
 +      regex => qr/.../,
 +      handler => 'method_name',
 +     },
 +   ]);
 +
 +A "unary operator" is a SQL syntactic clause that can be 
 +applied to a field - the operator goes before the field
 +
 +You can write your own operator handlers - supply a C<unary_ops>
 +argument to the C<new> method. That argument takes an arrayref of
 +operator definitions; each operator definition is a hashref with two
 +entries:
 +
 +=over
 +
 +=item regex
 +
 +the regular expression to match the operator
 +
 +=item handler
 +
 +Either a coderef or a plain scalar method name. In both cases
 +the expected return is C<< $sql >>.
 +
 +When supplied with a method name, it is simply called on the
 +L<SQL::Abstract/> object as:
 +
 + $self->$method_name ($op, $arg)
 +
 + Where:
 +
 +  $op is the part that matched the handler regex
 +  $arg is the RHS or argument of the operator
 +
 +When supplied with a coderef, it is called as:
 +
 + $coderef->($self, $op, $arg)
 +
 +
 +=back
 +
 +
  =head1 PERFORMANCE
  
  Thanks to some benchmarking by Mark Stosberg, it turns out that