Merge branch 'master' of git@github.com:ashb/sql-abstract
Rob Kinyon [Wed, 1 Apr 2009 13:21:56 +0000 (09:21 -0400)]
lib/SQL/Abstract.pm
lib/SQL/Abstract/AST/Compat.pm [deleted file]
lib/SQL/Abstract/AST/v1.pm
lib/SQL/Abstract/Compat.pm
lib/SQL/Abstract/Types/Compat.pm
t/001_basic.t
t/003_quote.t
t/100_expr_basic.t
t/101_expr_funcitons.t [new file with mode: 0644]
t/compat/00new.t [new file with mode: 0644]
t/compat/ast/01.t

index 848c01a..194792e 100644 (file)
@@ -31,8 +31,8 @@ class SQL::Abstract {
     '==' => '=',
     '!=' => '!=',
     # LIKE is always "field LIKE <value>"
-    '-like' => 'LIKE',
-    '-not_like' => 'NOT LIKE',
+    'like' => 'LIKE',
+    'not_like' => 'NOT LIKE',
   );
 
   has expr_dispatch_table => (
diff --git a/lib/SQL/Abstract/AST/Compat.pm b/lib/SQL/Abstract/AST/Compat.pm
deleted file mode 100644 (file)
index 97e2fd3..0000000
+++ /dev/null
@@ -1,127 +0,0 @@
-use MooseX::Declare;
-
-class SQL::Abstract::AST::Compat {
-
-  use MooseX::Types::Moose qw/ArrayRef HashRef Str ScalarRef/;
-  use SQL::Abstract::Types qw/AST/;
-  use SQL::Abstract::Types::Compat ':all';
-  use Devel::PartialDump qw/dump/;
-  use Carp qw/croak/;
-
-  clean;
-
-  has logic => (
-    is => 'rw',
-    isa => LogicEnum,
-    default => 'AND'
-  );
-
-  method generate(WhereType $ast) returns (AST) {
-    return $self->recurse_where($ast);
-  }
-
-  method recurse_where(WhereType $ast, LogicEnum $logic?) returns (AST) {
-    return $self->recurse_where_hash($logic || 'AND', $ast) if is_HashRef($ast);
-    return $self->recurse_where_array($logic || 'OR', $ast) if is_ArrayRef($ast);
-    croak "Unknown where clause type " . dump($ast);
-  }
-
-  method recurse_where_hash(LogicEnum $logic, HashRef $ast) returns (AST) {
-    my @args;
-    my $ret = {
-      -type => 'expr',
-      op => lc $logic,
-      args => \@args
-    };
-
-    while (my ($key,$value) = each %$ast) {
-      if ($key =~ /^-(or|and)$/) {
-        my $val = $self->recurse_where($value, uc $1);
-        if ($val->{op} eq $ret->{op}) {
-          push @args, @{$val->{args}};
-        }
-        else {
-          push @args, $val;
-        }
-        next;
-      }
-
-      push @args, $self->field($key, $value);
-    }
-
-    return $args[0] if @args == 1;
-
-    return $ret;
-  }
-
-  method recurse_where_array(LogicEnum $logic, ArrayRef $ast) returns (AST) {
-    my @args;
-    my $ret = {
-      -type => 'expr',
-      op => lc $logic,
-      args => \@args
-    };
-    my @nodes = @$ast;
-
-    while (my $key = shift @nodes) {
-      if ($key =~ /^-(or|and)$/) {
-        my $value = shift @nodes
-          or confess "missing value after $key at " . dump($ast);
-
-        my $val = $self->recurse_where($value, uc $1);
-        if ($val->{op} eq $ret->{op}) {
-          push @args, @{$val->{args}};
-        }
-        else {
-          push @args, $val;
-        }
-        next;
-      }
-
-      push @args, $self->recurse_where($key);
-    }
-
-    return $args[0] if @args == 1;
-
-    return $ret;
-  }
-
-  method field(Str $key, $value) returns (AST) {
-    my $ret = {
-      -type => 'expr',
-      op => '==',
-      args => [
-        { -type => 'name', args => [$key] }
-      ],
-    };
-
-    if (is_Str($value)) {
-      push @{$ret->{args}}, { -type => 'value', value => $value };
-    }
-
-    return $ret;
-  }
-
-
-};
-
-1;
-
-=head1 NAME
-
-SQL::Abstract::AST::Compat - v1.xx AST -> v2 AST visitor
-
-=head1 DESCRIPTION
-
-The purpose of this module is to take the where clause arguments from version
-1.x of SQL::Abstract, and turn it into a proper, explicit AST, suitable for use
-in the rest of the code.
-
-Please note that this module does not have the same interface as other
-SQL::Abstract ASTs.
-
-=head1 AUTHOR
-
-Ash Berlin C<< <ash@cpan.org> >>
-
-=cut
index 821245c..53db579 100644 (file)
@@ -19,6 +19,8 @@ class SQL::Abstract::AST::v1 extends SQL::Abstract {
       %{super()},
       in => $self->can('_in'),
       not_in => $self->can('_in'),
+      between => $self->can('_between'),
+      not_between => $self->can('_between'),
       and => $self->can('_recurse_where'),
       or => $self->can('_recurse_where'),
       map { +"$_" => $self->can("_$_") } qw/
@@ -130,17 +132,25 @@ class SQL::Abstract::AST::v1 extends SQL::Abstract {
     my $post;
     $post = pop @names if $names[-1] eq '*';
 
-    my $ret = 
-      $quote->[0] . 
-      join( $join, @names ) . 
-      $quote->[-1];
+    my $ret;
+    $ret = $quote->[0] . 
+           join( $join, @names ) . 
+           $quote->[-1]
+      if @names;
+
+    $ret = $ret 
+         ? $ret . $sep . $post
+         : $post
+      if defined $post;
+
 
-    $ret .= $sep . $post if defined $post;
     return $ret;
   }
 
 
   method _list(AST $ast) {
+    return "" unless $ast->{args};
+
     my @items = is_ArrayRef($ast->{args})
               ? @{$ast->{args}}
               : $ast->{args};
@@ -221,7 +231,12 @@ class SQL::Abstract::AST::v1 extends SQL::Abstract {
     croak "'$op' is not a valid AST type in an expression with " . dump($ast)
       if $ast->{-type} ne 'expr';
 
-    croak "'$op' is not a valid operator in an expression with " . dump($ast);
+    # This is an attempt to do some form of validation on function names. This
+    # might end up being a bad thing.
+    croak "'$op' is not a valid operator in an expression with " . dump($ast)
+      if $op =~ /\W/;
+
+    return $self->_generic_function_op($ast);
    
   }
 
@@ -235,6 +250,12 @@ class SQL::Abstract::AST::v1 extends SQL::Abstract {
     );
   }
 
+  method _generic_function_op(AST $ast) {
+    my $op = $ast->{op};
+
+    return "$op(" . $self->_list($ast) . ")";
+  }
+
   method _in(AST $ast) {
   
     my ($field,@values) = @{$ast->{args}};
@@ -250,7 +271,18 @@ class SQL::Abstract::AST::v1 extends SQL::Abstract {
            ")";
   }
 
-  method _generic_func(ArrayRef $ast) {
+  method _between(AST $ast) {
+  
+    my ($field,@values) = @{$ast->{args}};
+
+    my $not = ($ast->{op} =~ /^not_/) ? " NOT" : "";
+    croak "between requires 3 arguments: " . dump($ast)
+      unless @values == 2;
+
+    return $self->_expr($field) .
+           $not . 
+           " BETWEEN " .
+           join(" AND ", map { $self->dispatch($_) } @values );
   }
 
   # 'constants' that are portable across DBs
index 1c13897..5c7313a 100644 (file)
@@ -4,40 +4,242 @@ class SQL::Abstract::Compat {
 
   use Moose::Util::TypeConstraints;
   use MooseX::Types::Moose qw/Str ScalarRef ArrayRef HashRef/;
-  use MooseX::Types -declare => [qw/LogicEnum WhereType/];
-
-  enum LogicEnum, qw(OR AND);
-
-  subtype WhereType, as Str;
+  use SQL::Abstract::Types::Compat ':all';
+  use SQL::Abstract::Types qw/AST/;
+  use SQL::Abstract::AST::v1;
+  use Data::Dump qw/pp/;
+  use Devel::PartialDump qw/dump/;
+  use Carp qw/croak/;
 
+  class_type 'SQL::Abstract';
   clean;
 
   has logic => (
     is => 'rw',
     isa => LogicEnum,
-    default => 'AND'
+    default => 'AND',
+    coerce => 1,
+    required => 1,
+  );
+
+  has visitor => (
+    is => 'rw',
+    isa => 'SQL::Abstract',
+    clearer => 'clear_visitor',
+    lazy => 1,
+    builder => '_build_visitor',
   );
 
+  has cmp => (
+    is => 'rw',
+    isa => 'Str',
+    default => '=',
+    required => 1,
+  );
 
+  our %CMP_MAP = (
+    '=' => '==',
+  );
+
+  has convert => (
+    is => 'rw',
+    isa => 'Str',
+    predicate => 'has_field_convertor'
+  );
 
   method select(Str|ArrayRef|ScalarRef $from, ArrayRef|Str $fields,
-                Str|ScalarRef|ArrayRef|HashRef $where?,
-                Str|ScalarRef|ArrayRef|HashRef $order?) {
-    return ("", );
+                WhereType $where?,
+                WhereType $order?)
+  {
+    my $ast = {
+      -type => 'select',
+      columns => [ 
+        map {
+          $self->mk_name(0, $_)
+        } ( is_Str($fields) ? $fields : @$fields )
+      ],
+      tablespec => $self->tablespec($from)
+    };
+
+
+    $ast->{where} = $self->recurse_where($where)
+      if defined $where;
+
+    return ($self->visitor->dispatch($ast), $self->visitor->binds);
   }
 
-  method where(Str|ScalarRef|ArrayRef|HashRef $where,
-               Str|ScalarRef|ArrayRef|HashRef $order?) {
+  method where(WhereType $where,
+               WhereType $order?)
+  {
+    my $ret = "";
+    if ($where) {
+      my $ast = $self->recurse_where($where);
+      $ret .= "WHERE " . $self->visitor->_expr($ast);
+    }
 
-    my $ast = {
+    return $ret;
+  }
+
+  method _build_visitor() {
+    return SQL::Abstract->create(1);
+  } 
+
+  sub mk_name {
+    my ($self, $use_convert) = (shift,shift);
+    my $ast = { -type => 'name', args => [ @_ ] };
+
+    return $ast
+      unless $use_convert && $self->has_field_convertor;
+
+    return $self->apply_convert($ast);
+  }
+
+  method tablespec(Str|ArrayRef|ScalarRef $from) {
+    return $self->mk_name(0, $from)
+      if is_Str($from);
+  }
+
+  method recurse_where(WhereType $ast, LogicEnum $logic?) returns (AST) {
+    return $self->recurse_where_hash($logic || 'AND', $ast) if is_HashRef($ast);
+    return $self->recurse_where_array($logic || 'OR', $ast) if is_ArrayRef($ast);
+    croak "Unknown where clause type " . dump($ast);
+  }
+
+  method recurse_where_hash(LogicEnum $logic, HashRef $ast) returns (AST) {
+    my @args;
+    my $ret = {
+      -type => 'expr',
+      op => lc $logic,
+      args => \@args
+    };
+
+    while (my ($key,$value) = each %$ast) {
+      if ($key =~ /^-(or|and)$/) {
+        my $val = $self->recurse_where($value, uc $1);
+        if ($val->{op} eq $ret->{op}) {
+          push @args, @{$val->{args}};
+        }
+        else {
+          push @args, $val;
+        }
+        next;
+      }
+
+      push @args, $self->field($key, $value);
+    }
+
+    return $args[0] if @args == 1;
+
+    return $ret;
+  }
+
+  method recurse_where_array(LogicEnum $logic, ArrayRef $ast) returns (AST) {
+    my @args;
+    my $ret = {
       -type => 'expr',
+      op => lc $logic,
+      args => \@args
     };
+    my @nodes = @$ast;
+
+    while (my $key = shift @nodes) {
+      if ($key =~ /^-(or|and)$/) {
+        my $value = shift @nodes
+          or confess "missing value after $key at " . dump($ast);
+
+        my $val = $self->recurse_where($value, uc $1);
+        if ($val->{op} eq $ret->{op}) {
+          push @args, @{$val->{args}};
+        }
+        else {
+          push @args, $val;
+        }
+        next;
+      }
+
+      push @args, $self->recurse_where($key);
+    }
+
+    return $args[0] if @args == 1;
+
+    return $ret;
   }
 
-  method recurse_where(LogicEsnum $where) {
-    
+  method field(Str $key, $value) returns (AST) {
+    my $op = $CMP_MAP{$self->cmp} || $self->cmp;
+    my $ret = {
+      -type => 'expr',
+      op => $op,
+      args => [
+        $self->mk_name(1, $key)
+      ],
+    };
+
+    if (is_HashRef($value)) {
+      my ($op, @rest) = keys %$value;
+      confess "Don't know how to handle " . dump($value) . " (too many keys)"
+        if @rest;
+
+      # TODO: Validate the op?
+      if ($op =~ /^-([a-z_]+)$/i) {
+        $ret->{op} = lc $1;
+
+        if (is_ArrayRef($value->{$op})) {
+          push @{$ret->{args}}, $self->value($_)
+            for @{$value->{$op}};
+          return $ret;
+        }
+      }
+      else {
+        $ret->{op} = $op;
+      }
+
+      push @{$ret->{args}}, $self->value($value->{$op});
+
+    }
+    elsif (is_ArrayRef($value)) {
+      # Return an or clause, sort of.
+      return {
+        -type => 'expr',
+        op => 'or',
+        args => [ map {
+          {
+            -type => 'expr',
+            op => $op,
+            args => [
+              { -type => 'name', args => [$key] },
+              $self->value($_)
+            ],
+          }
+        } @$value ]
+      };
+    }
+    else {
+      push @{$ret->{args}}, $self->value($value);
+    }
+
+    return $ret;
+  }
+
+  method value($value) returns (AST) {
+    return $self->apply_convert( { -type => 'value', value => $value })
+      if is_Str($value);
+
+    confess "Don't know how to handle terminal value " . dump($value);
   }
 
+  method apply_convert(AST $ast) {
+    return $ast unless $self->has_field_convertor;
+
+    return {
+      -type => 'expr',
+      op => $self->convert,
+      args => [ $ast ]
+    };
+  }
+
+
 }
 
 =head1 NAME
index 36dd455..fff3859 100644 (file)
@@ -10,5 +10,7 @@ class SQL::Abstract::Types::Compat {
 
   enum LogicEnum, qw(OR AND);
 
+  coerce LogicEnum, from Str, via { uc $_ };
+
   subtype WhereType, as Str|ArrayRef|HashRef|ScalarRef;
 }
index 849a982..6eea38a 100644 (file)
@@ -1,7 +1,7 @@
 use strict;
 use warnings;
 
-use Test::More tests => 9;
+use Test::More tests => 10;
 use Test::Differences;
 
 use_ok('SQL::Abstract') or BAIL_OUT( "$@" );
@@ -13,6 +13,10 @@ my $sqla = SQL::Abstract->create(1);
 is $sqla->dispatch( { -type => 'name', args => [qw/me id/] }), "me.id",
   "Simple name generator";
 
+is $sqla->dispatch( { -type => 'name', args => ['*'] } ),
+   "*",
+   "* name generator";
+
 is $sqla->dispatch( { -type => 'name', args => [qw/me */]}),
    "me.*",
    "Simple name generator";
index 61df9dc..6f26fb6 100644 (file)
@@ -1,7 +1,7 @@
 use strict;
 use warnings;
  
-use Test::More tests => 2;
+use Test::More tests => 5;
 use Test::Exception;
  
 use_ok('SQL::Abstract') or BAIL_OUT( "$@" );
@@ -11,3 +11,18 @@ my $sqla = SQL::Abstract->create(1);
 lives_ok {
   $sqla->quote_chars('[]');
 } "coercion of quote_chars from Str works";
+
+
+is $sqla->dispatch( { -type => 'name', args => [qw/me id/] }), 
+   "[me].[id]",
+   "me.id";
+
+
+is $sqla->dispatch( { -type => 'name', args => [qw/me */] }), 
+   "[me].*",
+   "me.*";
+
+
+is $sqla->dispatch( { -type => 'name', args => [qw/*/] }), 
+   "*",
+   "*";
index f44d884..de43e64 100644 (file)
@@ -1,7 +1,7 @@
 use strict;
 use warnings;
 
-use Test::More tests => 13;
+use Test::More tests => 14;
 use Test::Differences;
 
 use_ok('SQL::Abstract') or BAIL_OUT( "$@" );
@@ -178,3 +178,15 @@ eq_or_diff(
   ],
   
   "NOT IN clause");
+
+
+is $sqla->dispatch(
+  { -type => 'expr',
+    op => 'like',
+    args => [
+      {-type => name => args => [qw/me id/] }, 
+      { -type => 'value', value => 500 }
+    ]
+  }
+), "me.id LIKE ?", 
+   "LIKE expr clause";
diff --git a/t/101_expr_funcitons.t b/t/101_expr_funcitons.t
new file mode 100644 (file)
index 0000000..13ec805
--- /dev/null
@@ -0,0 +1,51 @@
+
+use strict;
+use warnings;
+
+use Test::More tests => 4;
+use Test::Differences;
+
+use_ok('SQL::Abstract') or BAIL_OUT( "$@" );
+
+my $sqla = SQL::Abstract->create(1);
+
+is $sqla->dispatch(
+  { -type => 'expr',
+    op => '==',
+    args => [
+      { -type => 'expr',
+        op => 'ROUND',
+        args => [
+          {-type => name => args => [qw/me id/] }, 
+        ]
+      },
+      { -type => 'expr',
+        op => 'ROUND',
+        args => [
+          { -type => 'value', value => 500 }
+        ]
+      },
+    ]
+  }
+), "ROUND(me.id) = ROUND(?)", 
+   "simple expr clause";
+
+is $sqla->dispatch(
+  { -type => 'expr',
+    op => 'last_insert_id',
+  }
+), "last_insert_id()",
+   "last_insert_id";
+
+is $sqla->dispatch(
+  { -type => 'expr',
+    op => 'between',
+    args => [
+      {-type => name => args => [qw/me id/] }, 
+      { -type => 'value', value => 500 },
+      { -type => 'value', value => 599 },
+    ],
+  }
+), "me.id BETWEEN ? AND ?",
+   "between";
+
diff --git a/t/compat/00new.t b/t/compat/00new.t
new file mode 100644 (file)
index 0000000..b1cd6c8
--- /dev/null
@@ -0,0 +1,124 @@
+use strict;
+use warnings;
+use Test::More;
+
+use SQL::Abstract::Test import => ['is_same_sql_bind'];
+
+#LDNOTE: renamed all "bind" into "where" because that's what they are
+
+my @handle_tests = (
+      #1
+      {
+              args => {logic => 'OR'},
+#              stmt => 'SELECT * FROM test WHERE ( a = ? OR b = ? )'
+# LDNOTE: modified the line above (changing the test suite!!!) because
+# the test was not consistent with the doc: hashrefs should not be
+# influenced by the current logic, they always mean 'AND'. So 
+# { a => 4, b => 0} should ALWAYS mean ( a = ? AND b = ? ).
+#
+# acked by RIBASUSHI
+              stmt => 'SELECT * FROM test WHERE ( a = ? AND b = ? )'
+      },
+
+      #2
+      {
+              args => {},
+              stmt => 'SELECT * FROM test WHERE ( a = ? AND b = ? )'
+      },
+      #3
+      {
+              args => {case => "upper"},
+              stmt => 'SELECT * FROM test WHERE ( a = ? AND b = ? )'
+      },
+      #4
+      {
+              args => {case => "upper", cmp => "="},
+              stmt => 'SELECT * FROM test WHERE ( a = ? AND b = ? )'
+      },
+      #5
+      {
+              args => {cmp => "=", logic => 'or'},
+# LDNOTE idem
+#              stmt => 'SELECT * FROM test WHERE ( a = ? OR b = ? )'
+# acked by RIBASUSHI
+              stmt => 'SELECT * FROM test WHERE ( a = ? AND b = ? )'
+      },
+      #6
+      {
+              args => {cmp => "like"},
+              stmt => 'SELECT * FROM test WHERE ( a LIKE ? AND b LIKE ? )'
+      },
+      #7
+      {
+              args => {logic => "or", cmp => "like"},
+# LDNOTE idem
+#              stmt => 'SELECT * FROM test WHERE ( a LIKE ? OR b LIKE ? )'
+# acked by RIBASUSHI
+              stmt => 'SELECT * FROM test WHERE ( a LIKE ? AND b LIKE ? )'
+      },
+      #8
+      {
+              todo => 'lower',
+              args => {case => "lower"},
+              stmt => 'select * from test where ( a = ? and b = ? )'
+      },
+      #9
+      {
+              todo => 'lower',
+              args => {case => "lower", cmp => "="},
+              stmt => 'select * from test where ( a = ? and b = ? )'
+      },
+      #10
+      {
+              todo => 'lower',
+              args => {case => "lower", cmp => "like"},
+              stmt => 'select * from test where ( a like ? and b like ? )'
+      },
+      #11
+      {
+              todo => 'lower',
+              args => {case => "lower", convert => "lower", cmp => "like"},
+              stmt => 'select * from test where ( lower(a) like lower(?) and lower(b) like lower(?) )'
+      },
+      #12
+      {
+              args => {convert => "Round"},
+              stmt => 'SELECT * FROM test WHERE ( ROUND(a) = ROUND(?) AND ROUND(b) = ROUND(?) )',
+      },
+      #13
+      {
+              todo => 'lower',
+              args => {convert => "lower"},
+              stmt => 'SELECT * FROM test WHERE ( ( LOWER(ticket) = LOWER(?) ) OR ( LOWER(hostname) = LOWER(?) ) OR ( LOWER(taco) = LOWER(?) ) OR ( LOWER(salami) = LOWER(?) ) )',
+              where => [ { ticket => 11 }, { hostname => 11 }, { taco => 'salad' }, { salami => 'punch' } ],
+      },
+      #14
+      {
+              args => {convert => "upper"},
+              stmt => 'SELECT * FROM test WHERE ( ( UPPER(hostname) IN ( UPPER(?), UPPER(?), UPPER(?), UPPER(?) ) AND ( ( UPPER(ticket) = UPPER(?) ) OR ( UPPER(ticket) = UPPER(?) ) OR ( UPPER(ticket) = UPPER(?) ) ) ) OR ( UPPER(tack) BETWEEN UPPER(?) AND UPPER(?) ) OR ( ( ( UPPER(a) = UPPER(?) ) OR ( UPPER(a) = UPPER(?) ) OR ( UPPER(a) = UPPER(?) ) ) AND ( ( UPPER(e) != UPPER(?) ) OR ( UPPER(e) != UPPER(?) ) ) AND UPPER(q) NOT IN ( UPPER(?), UPPER(?), UPPER(?), UPPER(?), UPPER(?), UPPER(?), UPPER(?) ) ) )',
+              where => [ { ticket => [11, 12, 13], 
+                           hostname => { in => ['ntf', 'avd', 'bvd', '123'] } },
+                        { tack => { between => [qw/tick tock/] } },
+                        { a => [qw/b c d/], 
+                          e => { '!=', [qw(f g)] }, 
+                          q => { 'not in', [14..20] } } ],
+      },
+
+);
+
+plan tests => (1 + scalar(@handle_tests));
+
+use_ok('SQL::Abstract::Compat');
+
+for (@handle_tests) {
+  my $sql  = SQL::Abstract::Compat->new($_->{args});
+  my $where = $_->{where} || { a => 4, b => 0};
+  my($stmt, @bind) = $sql->select('test', '*', $where);
+
+  local $TODO = $_->{todo};
+
+  # LDNOTE: this original test suite from NWIGER did no comparisons
+  # on @bind values, just checking if @bind is nonempty.
+  # So here we just fake a [1] bind value for the comparison.
+  is_same_sql_bind($stmt, [@bind ? 1 : 0], $_->{stmt}, [1]);
+}
index 16397b9..55e49c7 100644 (file)
@@ -1,39 +1,29 @@
 use strict;
 use warnings;
 
-use SQL::Abstract::AST::Compat;
+use SQL::Abstract::Compat;
 
-use Test::More tests => 6;
+use Test::More tests => 12;
 use Test::Differences;
 
-ok(my $visitor = SQL::Abstract::AST::Compat->new);
+ok(my $visitor = SQL::Abstract::Compat->new);
 
-my $foo_eq_1 = {
-  -type => 'expr',
-  op => '==',
-  args => [
-    { -type => 'name', args => [qw/foo/] }, 
-    { -type => 'value', value => 1 }
-  ]
-};
+
+my $foo_id = { -type => 'name', args => [qw/foo/] };
+my $bar_id = { -type => 'name', args => [qw/bar/] };
+
+my $foo_eq_1 = field_op_value($foo_id, '==', 1);
+my $bar_eq_str = field_op_value($bar_id, '==', 'some str');
 
 eq_or_diff
-  $visitor->generate({ foo => 1 }),
+  $visitor->recurse_where({ foo => 1 }),
   $foo_eq_1,
   "Single value hash";
 
 
-my $bar_eq_str = {
-  -type => 'expr',
-  op => '==',
-  args => [
-    { -type => 'name', args => [qw/bar/] }, 
-    { -type => 'value', value => 'some str' }
-  ]
-};
 
 eq_or_diff
-  $visitor->generate({ foo => 1, bar => 'some str' }),
+  $visitor->recurse_where({ foo => 1, bar => 'some str' }),
   { -type => 'expr',
     op => 'and',
     args => [
@@ -44,7 +34,7 @@ eq_or_diff
   "two keys in hash";
 
 eq_or_diff
-  $visitor->generate({ -or => { foo => 1, bar => 'some str' } }),
+  $visitor->recurse_where({ -or => { foo => 1, bar => 'some str' } }),
   { -type => 'expr',
     op => 'or',
     args => [
@@ -56,7 +46,7 @@ eq_or_diff
 
 
 eq_or_diff
-  $visitor->generate([ -and => { foo => 1, bar => 'some str' } ]),
+  $visitor->recurse_where([ -and => { foo => 1, bar => 'some str' } ]),
   { -type => 'expr',
     op => 'and',
     args => [
@@ -68,7 +58,7 @@ eq_or_diff
 
 
 eq_or_diff
-  $visitor->generate([ -and => { foo => 1, bar => 'some str' }, { foo => 1} ]),
+  $visitor->recurse_where([ -and => { foo => 1, bar => 'some str' }, { foo => 1} ]),
   { -type => 'expr',
     op => 'or',
     args => [
@@ -82,4 +72,112 @@ eq_or_diff
       $foo_eq_1,
     ]
   },
-  "-and as first element of array";
+  "-and as first element of array + hash";
+
+eq_or_diff
+  $visitor->recurse_where({ foo => { '!=' => 'bar' } }),
+  field_op_value($foo_id, '!=', 'bar'),
+  "foo => { '!=' => 'bar' }";
+
+eq_or_diff
+  $visitor->recurse_where({ foo => [ 1, 'bar' ] }),
+  { -type => 'expr',
+    op => 'or',
+    args => [
+      $foo_eq_1,
+      field_op_value($foo_id, '==', 'bar'),
+    ],
+  },
+  "foo => [ 1, 'bar' ]";
+
+eq_or_diff
+  $visitor->recurse_where({ foo => { -in => [ 1, 'bar' ] } }),
+  { -type => 'expr',
+    op => 'in',
+    args => [
+      $foo_id,
+      { -type => 'value', value => 1 },
+      { -type => 'value', value => 'bar' },
+    ]
+  },
+  "foo => { -in => [ 1, 'bar' ] }";
+
+eq_or_diff
+  $visitor->recurse_where({ foo => { -not_in => [ 1, 'bar' ] } }),
+  { -type => 'expr',
+    op => 'not_in',
+    args => [
+      $foo_id,
+      { -type => 'value', value => 1 },
+      { -type => 'value', value => 'bar' },
+    ]
+  },
+  "foo => { -not_in => [ 1, 'bar' ] }";
+
+eq_or_diff
+  $visitor->recurse_where({ foo => { -in => [ ] } }),
+  { -type => 'expr',
+    op => 'in',
+    args => [
+      $foo_id,
+    ]
+  },
+  "foo => { -in => [ ] }";
+
+my $worker_eq = sub {
+  return { 
+    -type => 'expr',
+    op => '==',
+    args => [
+      { -type => 'name', args => ['worker'] },
+      { -type => 'value', value => $_[0] },
+    ],
+  }
+};
+eq_or_diff
+  $visitor->recurse_where( {
+    requestor => 'inna',
+    worker => ['nwiger', 'rcwe', 'sfz'],
+    status => { '!=', 'completed' }
+  } ),
+  { -type => 'expr',
+    op => 'and',
+    args => [
+      field_op_value(qw/status != completed/), 
+      { -type => 'expr',
+        op => 'or',
+        args => [
+          field_op_value(qw/worker == nwiger/), 
+          field_op_value(qw/worker == rcwe/), 
+          field_op_value(qw/worker == sfz/), 
+        ]
+      },
+      field_op_value(qw/requestor == inna/),
+    ]
+  },
+  "complex expr #1";
+
+
+
+sub field_op_value {
+  my ($field, $op, $value) = @_;
+
+  $field = ref $field eq 'HASH'
+         ? $field
+         : ref $field eq 'ARRAY' 
+         ? { -type => 'name', args => $field } 
+         : { -type => 'name', args => [$field] };
+
+  $value = ref $value eq 'HASH'
+         ? $value
+         : { -type => 'value', value => $value };
+
+  return {
+    -type => 'expr',
+    op => $op,
+    args => [
+      $field,
+      $value
+    ]
+  };
+}