Merged changes from the '1.50_RC-extraparens' branch.
Norbert Buchmuller [Mon, 15 Dec 2008 21:26:39 +0000 (21:26 +0000)]
lib/SQL/Abstract/Test.pm
t/10test.t

index 4153add..6c47bcf 100644 (file)
@@ -16,13 +16,63 @@ our $case_sensitive = 0;
 our $sql_differ; # keeps track of differing portion between SQLs
 our $tb = __PACKAGE__->builder;
 
+# Parser states for _recurse_parse()
+use constant {
+  PARSE_TOP_LEVEL => 0,
+  PARSE_IN_EXPR => 1,
+  PARSE_IN_PARENS => 2,
+};
+
+# These SQL keywords always signal end of the current expression (except inside
+# of a parenthesized subexpression).
+# Format: A list of strings that will be compiled to extended syntax (ie.
+# /.../x) regexes, without capturing parentheses. They will be automatically
+# anchored to word boundaries to match the whole token).
+my @expression_terminator_sql_keywords = (
+  'FROM',
+  '(?:
+    (?:
+        (?: \b (?: LEFT | RIGHT | FULL ) \s+ )?
+        (?: \b (?: CROSS | INNER | OUTER ) \s+ )?
+    )?
+    JOIN
+  )',
+  'ON',
+  'WHERE',
+  'GROUP \s+ BY',
+  'HAVING',
+  'ORDER \s+ BY',
+  'LIMIT',
+  'OFFSET',
+  'FOR',
+  'UNION',
+  'INTERSECT',
+  'EXCEPT',
+);
+
+my $tokenizer_re_str = join('|',
+  map { '\b' . $_ . '\b' }
+    @expression_terminator_sql_keywords, 'AND', 'OR'
+);
+
+my $tokenizer_re = qr/
+  \s*
+  (
+      \(
+    |
+      \)
+    |
+      $tokenizer_re_str
+  )
+  \s*
+/xi;
+
+
 sub is_same_sql_bind {
   my ($sql1, $bind_ref1, $sql2, $bind_ref2, $msg) = @_;
 
   # compare
-  my $tree1     = parse($sql1);
-  my $tree2     = parse($sql2);
-  my $same_sql  = eq_sql($tree1, $tree2);
+  my $same_sql  = eq_sql($sql1, $sql2);
   my $same_bind = eq_bind($bind_ref1, $bind_ref2);
 
   # call Test::Builder::ok
@@ -51,6 +101,16 @@ sub eq_bind {
 }
 
 sub eq_sql {
+  my ($sql1, $sql2) = @_;
+
+  # parse
+  my $tree1 = parse($sql1);
+  my $tree2 = parse($sql2);
+
+  return _eq_sql($tree1, $tree2);
+}
+
+sub _eq_sql {
   my ($left, $right) = @_;
 
   # ignore top-level parentheses 
@@ -74,8 +134,8 @@ sub eq_sql {
       return $eq;
     }
     else { # binary operator
-      return eq_sql($left->[1][0], $right->[1][0])  # left operand
-          && eq_sql($left->[1][1], $right->[1][1]); # right operand
+      return _eq_sql($left->[1][0], $right->[1][0])  # left operand
+          && _eq_sql($left->[1][1], $right->[1][1]); # right operand
     }
   }
 }
@@ -84,27 +144,37 @@ sub eq_sql {
 sub parse {
   my $s = shift;
 
-  # tokenize string
-  my $tokens = [grep {!/^\s*$/} split /\s*(\(|\)|\bAND\b|\bOR\b)\s*/, $s];
+  # tokenize string, and remove all optional whitespace
+  my $tokens = [];
+  foreach my $token (split $tokenizer_re, $s) {
+    $token =~ s/\s+/ /g;
+    $token =~ s/\s+([^\w\s])/$1/g;
+    $token =~ s/([^\w\s])\s+/$1/g;
+    push @$tokens, $token if $token !~ /^$/;
+  }
 
-  my $tree = _recurse_parse($tokens);
+  my $tree = _recurse_parse($tokens, PARSE_TOP_LEVEL);
   return $tree;
 }
 
 sub _recurse_parse {
-  my $tokens = shift;
+  my ($tokens, $state) = @_;
 
   my $left;
   while (1) { # left-associative parsing
 
     my $lookahead = $tokens->[0];
-    return $left if !defined($lookahead) || $lookahead eq ')';
+    return $left if !defined($lookahead)
+      || ($state == PARSE_IN_PARENS && $lookahead eq ')')
+      || ($state == PARSE_IN_EXPR && grep { $lookahead =~ /^$_$/xi }
+            '\)', @expression_terminator_sql_keywords
+         );
 
     my $token = shift @$tokens;
 
     # nested expression in ()
     if ($token eq '(') {
-      my $right = _recurse_parse($tokens);
+      my $right = _recurse_parse($tokens, PARSE_IN_PARENS);
       $token = shift @$tokens   or croak "missing ')'";
       $token eq ')'             or croak "unexpected token : $token";
       $left = $left ? [CONCAT => [$left, [PAREN => $right]]]
@@ -112,9 +182,15 @@ sub _recurse_parse {
     }
     # AND/OR
     elsif ($token eq 'AND' || $token eq 'OR')  {
-      my $right = _recurse_parse($tokens);
+      my $right = _recurse_parse($tokens, PARSE_IN_EXPR);
       $left = [$token => [$left, $right]];
     }
+    # expression terminator keywords (as they start a new expression)
+    elsif (grep { $token =~ /^$_$/xi } @expression_terminator_sql_keywords) {
+      my $right = _recurse_parse($tokens, PARSE_IN_EXPR);
+      $left = $left ? [CONCAT => [$left, [CONCAT => [[EXPR => $token], [PAREN => $right]]]]]
+                    : [CONCAT => [[EXPR => $token], [PAREN  => $right]]];
+    }
     # leaf expression
     else {
       $left = $left ? [CONCAT => [$left, [EXPR => $token]]]
@@ -220,10 +296,12 @@ where a difference was encountered.
 
 L<SQL::Abstract>, L<Test::More>, L<Test::Builder>.
 
-=head1 AUTHOR
+=head1 AUTHORS
 
 Laurent Dami, E<lt>laurent.dami AT etat  geneve  chE<gt>
 
+Norbert Buchmuller <norbi@nix.hu>
+
 =head1 COPYRIGHT AND LICENSE
 
 Copyright 2008 by Laurent Dami.
index af6404d..f114d51 100644 (file)
 use strict;
 use warnings;
 use List::Util qw(sum);
-use Data::Dumper;
 
 use Test::More;
 
 
+my @sql_tests = (
+      # WHERE condition - equal
+      {
+        equal => 1,
+        statements => [
+          q/SELECT foo FROM bar WHERE a = 1/,
+          q/SELECT foo FROM bar WHERE a=1/,
+          q/SELECT foo FROM bar WHERE (a = 1)/,
+          q/SELECT foo FROM bar WHERE (a=1)/,
+          q/SELECT foo FROM bar WHERE ( a = 1 )/,
+          q/
+            SELECT
+              foo
+            FROM
+              bar
+            WHERE
+              a = 1
+          /,
+          q/
+            SELECT
+              foo
+            FROM
+              bar
+            WHERE
+              (a = 1)
+          /,
+          q/
+            SELECT
+              foo
+            FROM
+              bar
+            WHERE
+              ( a = 1 )
+          /,
+          q/SELECT foo FROM bar WHERE ((a = 1))/,
+          q/SELECT foo FROM bar WHERE ( (a = 1) )/,
+          q/SELECT foo FROM bar WHERE ( ( a = 1 ) )/,
+        ]
+      },
+      {
+        equal => 1,
+        statements => [
+          q/SELECT foo FROM bar WHERE a = 1 AND b = 1/,
+          q/SELECT foo FROM bar WHERE (a = 1) AND (b = 1)/,
+          q/SELECT foo FROM bar WHERE ((a = 1) AND (b = 1))/,
+          q/SELECT foo FROM bar WHERE (a = 1 AND b = 1)/,
+          q/SELECT foo FROM bar WHERE ((a = 1 AND b = 1))/,
+          q/SELECT foo FROM bar WHERE (((a = 1) AND (b = 1)))/,
+          q/
+            SELECT
+              foo
+            FROM
+              bar
+            WHERE
+              a = 1
+              AND
+              b = 1
+          /,
+          q/
+            SELECT
+              foo
+            FROM
+              bar
+            WHERE
+              (a = 1
+              AND
+              b = 1)
+          /,
+          q/
+            SELECT
+              foo
+            FROM
+              bar
+            WHERE
+              (a = 1)
+              AND
+              (b = 1)
+          /,
+          q/
+            SELECT
+              foo
+            FROM
+              bar
+            WHERE
+              ((a = 1)
+              AND
+              (b = 1))
+          /,
+        ]
+      },
+
+      # WHERE condition - different
+      {
+        equal => 0,
+        statements => [
+          q/SELECT foo FROM bar WHERE a = 1/,
+          q/SELECT quux FROM bar WHERE a = 1/,
+          q/SELECT foo FROM quux WHERE a = 1/,
+          q/FOOBAR foo FROM bar WHERE a = 1/,
+
+          q/SELECT foo FROM bar WHERE a = 2/,
+          q/SELECT foo FROM bar WHERE a < 1/,
+          q/SELECT foo FROM bar WHERE b = 1/,
+          q/SELECT foo FROM bar WHERE (c = 1)/,
+          q/SELECT foo FROM bar WHERE (d = 1)/,
+
+          q/SELECT foo FROM bar WHERE a = 1 AND quux/,
+          q/SELECT foo FROM bar WHERE a = 1 GROUP BY foo/,
+          q/SELECT foo FROM bar WHERE a = 1 ORDER BY foo/,
+          q/SELECT foo FROM bar WHERE a = 1 LIMIT 1/,
+          q/SELECT foo FROM bar WHERE a = 1 OFFSET 1/,
+          q/SELECT foo FROM bar JOIN quux WHERE a = 1/,
+          q/SELECT foo FROM bar JOIN quux ON a = 1 WHERE a = 1/,
+        ]
+      },
+      {
+        equal => 0,
+        statements => [
+          q/SELECT foo FROM bar WHERE a = 1 AND b = 1/,
+          q/SELECT quux FROM bar WHERE a = 1 AND b = 1/,
+          q/SELECT foo FROM quux WHERE a = 1 AND b = 1/,
+          q/FOOBAR foo FROM bar WHERE a = 1 AND b = 1/,
+
+          q/SELECT foo FROM bar WHERE a = 2 AND b = 1/,
+          q/SELECT foo FROM bar WHERE a = 3 AND (b = 1)/,
+          q/SELECT foo FROM bar WHERE (a = 4) AND b = 1/,
+          q/SELECT foo FROM bar WHERE (a = 5) AND (b = 1)/,
+          q/SELECT foo FROM bar WHERE ((a = 6) AND (b = 1))/,
+          q/SELECT foo FROM bar WHERE ((a = 7) AND (b = 1))/,
+
+          q/SELECT foo FROM bar WHERE a = 1 AND b = 2/,
+          q/SELECT foo FROM bar WHERE a = 1 AND (b = 3)/,
+          q/SELECT foo FROM bar WHERE (a = 1) AND b = 4/,
+          q/SELECT foo FROM bar WHERE (a = 1) AND (b = 5)/,
+          q/SELECT foo FROM bar WHERE ((a = 1) AND (b = 6))/,
+          q/SELECT foo FROM bar WHERE ((a = 1) AND (b = 7))/,
+
+          q/SELECT foo FROM bar WHERE a < 1 AND b = 1/,
+          q/SELECT foo FROM bar WHERE b = 1 AND b = 1/,
+          q/SELECT foo FROM bar WHERE (c = 1) AND b = 1/,
+          q/SELECT foo FROM bar WHERE (d = 1) AND b = 1/,
+
+          q/SELECT foo FROM bar WHERE a = 1 AND b = 1 AND quux/,
+          q/SELECT foo FROM bar WHERE a = 1 AND b = 1 GROUP BY foo/,
+          q/SELECT foo FROM bar WHERE a = 1 AND b = 1 ORDER BY foo/,
+          q/SELECT foo FROM bar WHERE a = 1 AND b = 1 LIMIT 1/,
+          q/SELECT foo FROM bar WHERE a = 1 AND b = 1 OFFSET 1/,
+          q/SELECT foo FROM bar JOIN quux WHERE a = 1 AND b = 1/,
+          q/SELECT foo FROM bar JOIN quux ON a = 1 WHERE a = 1 AND b = 1/,
+        ]
+      },
+
+      # JOIN condition - equal
+      {
+        equal => 1,
+        statements => [
+          q/SELECT foo FROM bar JOIN baz ON a = 1 WHERE x = 1/,
+          q/SELECT foo FROM bar JOIN baz ON a=1 WHERE x = 1/,
+          q/SELECT foo FROM bar JOIN baz ON (a = 1) WHERE x = 1/,
+          q/SELECT foo FROM bar JOIN baz ON (a=1) WHERE x = 1/,
+          q/SELECT foo FROM bar JOIN baz ON ( a = 1 ) WHERE x = 1/,
+          q/
+            SELECT
+              foo
+            FROM
+              bar
+            JOIN
+              baz
+            ON
+              a = 1
+            WHERE
+              x = 1
+          /,
+          q/
+            SELECT
+              foo
+            FROM
+              bar
+            JOIN
+              baz
+            ON
+              (a = 1)
+            WHERE
+              x = 1
+          /,
+          q/
+            SELECT
+              foo
+            FROM
+              bar
+            JOIN
+              baz
+            ON
+              ( a = 1 )
+            WHERE
+              x = 1
+          /,
+          q/SELECT foo FROM bar JOIN baz ON ((a = 1)) WHERE x = 1/,
+          q/SELECT foo FROM bar JOIN baz ON ( (a = 1) ) WHERE x = 1/,
+          q/SELECT foo FROM bar JOIN baz ON ( ( a = 1 ) ) WHERE x = 1/,
+        ]
+      },
+      {
+        equal => 1,
+        statements => [
+          q/SELECT foo FROM bar JOIN baz ON a = 1 AND b = 1 WHERE x = 1/,
+          q/SELECT foo FROM bar JOIN baz ON (a = 1) AND (b = 1) WHERE x = 1/,
+          q/SELECT foo FROM bar JOIN baz ON ((a = 1) AND (b = 1)) WHERE x = 1/,
+          q/SELECT foo FROM bar JOIN baz ON (a = 1 AND b = 1) WHERE x = 1/,
+          q/SELECT foo FROM bar JOIN baz ON ((a = 1 AND b = 1)) WHERE x = 1/,
+          q/SELECT foo FROM bar JOIN baz ON (((a = 1) AND (b = 1))) WHERE x = 1/,
+          q/
+            SELECT
+              foo
+            FROM
+              bar
+            JOIN
+              baz
+            ON
+              a = 1
+              AND
+              b = 1
+            WHERE
+              x = 1
+          /,
+          q/
+            SELECT
+              foo
+            FROM
+              bar
+            JOIN
+              baz
+            ON
+              (a = 1
+              AND
+              b = 1)
+            WHERE
+              x = 1
+          /,
+          q/
+            SELECT
+              foo
+            FROM
+              bar
+            JOIN
+              baz
+            ON
+              (a = 1)
+              AND
+              (b = 1)
+            WHERE
+              x = 1
+          /,
+          q/
+            SELECT
+              foo
+            FROM
+              bar
+            JOIN
+              baz
+            ON
+              ((a = 1)
+              AND
+              (b = 1))
+            WHERE
+              x = 1
+          /,
+        ]
+      },
+
+      # JOIN condition - different
+      {
+        equal => 0,
+        statements => [
+          q/SELECT foo FROM bar JOIN quux ON a = 1 WHERE quuux/,
+          q/SELECT quux FROM bar JOIN quux ON a = 1 WHERE quuux/,
+          q/SELECT foo FROM quux JOIN quux ON a = 1 WHERE quuux/,
+          q/FOOBAR foo FROM bar JOIN quux ON a = 1 WHERE quuux/,
+
+          q/SELECT foo FROM bar JOIN quux ON a = 2 WHERE quuux/,
+          q/SELECT foo FROM bar JOIN quux ON a < 1 WHERE quuux/,
+          q/SELECT foo FROM bar JOIN quux ON b = 1 WHERE quuux/,
+          q/SELECT foo FROM bar JOIN quux ON (c = 1) WHERE quuux/,
+          q/SELECT foo FROM bar JOIN quux ON (d = 1) WHERE quuux/,
+
+          q/SELECT foo FROM bar JOIN quux ON a = 1 AND quuux/,
+          q/SELECT foo FROM bar JOIN quux ON a = 1 GROUP BY foo/,
+          q/SELECT foo FROM bar JOIN quux ON a = 1 ORDER BY foo/,
+          q/SELECT foo FROM bar JOIN quux ON a = 1 LIMIT 1/,
+          q/SELECT foo FROM bar JOIN quux ON a = 1 OFFSET 1/,
+          q/SELECT foo FROM bar JOIN quux ON a = 1 JOIN quuux/,
+          q/SELECT foo FROM bar JOIN quux ON a = 1 JOIN quuux ON a = 1/,
+        ]
+      },
+      {
+        equal => 0,
+        statements => [
+          q/SELECT foo FROM bar JOIN quux ON a = 1 AND b = 1 WHERE quuux/,
+          q/SELECT quux FROM bar JOIN quux ON a = 1 AND b = 1 WHERE quuux/,
+          q/SELECT foo FROM quux JOIN quux ON a = 1 AND b = 1 WHERE quuux/,
+          q/FOOBAR foo FROM bar JOIN quux ON a = 1 AND b = 1 WHERE quuux/,
+
+          q/SELECT foo FROM bar JOIN quux ON a = 2 AND b = 1 WHERE quuux/,
+          q/SELECT foo FROM bar JOIN quux ON a = 3 AND (b = 1) WHERE quuux/,
+          q/SELECT foo FROM bar JOIN quux ON (a = 4) AND b = 1 WHERE quuux/,
+          q/SELECT foo FROM bar JOIN quux ON (a = 5) AND (b = 1) WHERE quuux/,
+          q/SELECT foo FROM bar JOIN quux ON ((a = 6) AND (b = 1)) WHERE quuux/,
+          q/SELECT foo FROM bar JOIN quux ON ((a = 7) AND (b = 1)) WHERE quuux/,
+
+          q/SELECT foo FROM bar JOIN quux ON a = 1 AND b = 2 WHERE quuux/,
+          q/SELECT foo FROM bar JOIN quux ON a = 1 AND (b = 3) WHERE quuux/,
+          q/SELECT foo FROM bar JOIN quux ON (a = 1) AND b = 4 WHERE quuux/,
+          q/SELECT foo FROM bar JOIN quux ON (a = 1) AND (b = 5) WHERE quuux/,
+          q/SELECT foo FROM bar JOIN quux ON ((a = 1) AND (b = 6)) WHERE quuux/,
+          q/SELECT foo FROM bar JOIN quux ON ((a = 1) AND (b = 7)) WHERE quuux/,
+
+          q/SELECT foo FROM bar JOIN quux ON a < 1 AND b = 1 WHERE quuux/,
+          q/SELECT foo FROM bar JOIN quux ON b = 1 AND b = 1 WHERE quuux/,
+          q/SELECT foo FROM bar JOIN quux ON (c = 1) AND b = 1 WHERE quuux/,
+          q/SELECT foo FROM bar JOIN quux ON (d = 1) AND b = 1 WHERE quuux/,
+
+          q/SELECT foo FROM bar JOIN quux ON a = 1 AND b = 1 AND quuux/,
+          q/SELECT foo FROM bar JOIN quux ON a = 1 AND b = 1 GROUP BY foo/,
+          q/SELECT foo FROM bar JOIN quux ON a = 1 AND b = 1 ORDER BY foo/,
+          q/SELECT foo FROM bar JOIN quux ON a = 1 AND b = 1 LIMIT 1/,
+          q/SELECT foo FROM bar JOIN quux ON a = 1 AND b = 1 OFFSET 1/,
+          q/SELECT foo FROM bar JOIN quux JOIN quuux ON a = 1 AND b = 1/,
+          q/SELECT foo FROM bar JOIN quux ON a = 1 JOIN quuux ON a = 1 AND b = 1/,
+        ]
+      },
+
+      # DISTINCT ON (...) not confused with JOIN ON (...)
+      {
+        equal => 1,
+        statements => [
+          q/SELECT DISTINCT ON (foo, quux) foo, quux FROM bar WHERE a = 1/,
+          q/SELECT DISTINCT ON (foo, quux) foo, quux FROM bar WHERE a=1/,
+          q/SELECT DISTINCT ON (foo, quux) foo, quux FROM bar WHERE (a = 1)/,
+          q/SELECT DISTINCT ON (foo, quux) foo, quux FROM bar WHERE (a=1)/,
+          q/SELECT DISTINCT ON (foo, quux) foo, quux FROM bar WHERE ( a = 1 )/,
+          q/
+            SELECT DISTINCT ON (foo, quux)
+              foo,
+              quux
+            FROM
+              bar
+            WHERE
+              a = 1
+          /,
+          q/
+            SELECT DISTINCT ON (foo, quux)
+              foo,
+              quux
+            FROM
+              bar
+            WHERE
+              (a = 1)
+          /,
+          q/
+            SELECT DISTINCT ON (foo, quux)
+              foo,
+              quux
+            FROM
+              bar
+            WHERE
+              ( a = 1 )
+          /,
+          q/SELECT DISTINCT ON (foo, quux) foo, quux FROM bar WHERE ((a = 1))/,
+          q/SELECT DISTINCT ON (foo, quux) foo, quux FROM bar WHERE ( (a = 1) )/,
+          q/SELECT DISTINCT ON (foo, quux) foo, quux FROM bar WHERE ( ( a = 1 ) )/,
+        ]
+      },
+
+      # subselects - equal
+      {
+        equal => 1,
+        statements => [
+          q/SELECT * FROM (SELECT * FROM bar WHERE b = 1) AS foo WHERE a = 1/,
+          q/SELECT * FROM (SELECT * FROM bar WHERE b = 1) AS foo WHERE (a = 1)/,
+          q/SELECT * FROM (SELECT * FROM bar WHERE (b = 1)) AS foo WHERE a = 1/,
+          q/SELECT * FROM (SELECT * FROM bar WHERE (b = 1)) AS foo WHERE (a = 1)/,
+        ]
+      },
+      {
+        equal => 1,
+        statements => [
+          q/SELECT * FROM (SELECT * FROM bar WHERE b = 1 AND c = 1) AS foo WHERE a = 1/,
+          q/SELECT * FROM (SELECT * FROM bar WHERE b = 1 AND (c = 1)) AS foo WHERE a = 1/,
+          q/SELECT * FROM (SELECT * FROM bar WHERE (b = 1) AND c = 1) AS foo WHERE a = 1/,
+          q/SELECT * FROM (SELECT * FROM bar WHERE (b = 1) AND (c = 1)) AS foo WHERE a = 1/,
+          q/SELECT * FROM (SELECT * FROM bar WHERE ((b = 1) AND (c = 1))) AS foo WHERE a = 1/,
+
+          q/SELECT * FROM (SELECT * FROM bar WHERE b = 1 AND c = 1) AS foo WHERE (a = 1)/,
+          q/SELECT * FROM (SELECT * FROM bar WHERE b = 1 AND (c = 1)) AS foo WHERE (a = 1)/,
+          q/SELECT * FROM (SELECT * FROM bar WHERE (b = 1) AND c = 1) AS foo WHERE (a = 1)/,
+          q/SELECT * FROM (SELECT * FROM bar WHERE (b = 1) AND (c = 1)) AS foo WHERE (a = 1)/,
+          q/SELECT * FROM (SELECT * FROM bar WHERE ((b = 1) AND (c = 1))) AS foo WHERE (a = 1)/,
+        ]
+      },
+
+      # subselects - different
+      {
+        equal => 0,
+        statements => [
+          q/SELECT * FROM (SELECT * FROM bar WHERE b = 1) AS foo WHERE a = 1/,
+          q/SELECT * FROM (SELECT * FROM bar WHERE b = 1) AS foo WHERE a = 2/,
+          q/SELECT * FROM (SELECT * FROM bar WHERE b = 1) AS foo WHERE (a = 3)/,
+          q/SELECT * FROM (SELECT * FROM bar WHERE (b = 1)) AS foo WHERE a = 4/,
+          q/SELECT * FROM (SELECT * FROM bar WHERE (b = 1)) AS foo WHERE (a = 5)/,
+          q/SELECT * FROM (SELECT * FROM bar WHERE b = 2) AS foo WHERE a = 1/,
+          q/SELECT * FROM (SELECT * FROM bar WHERE b = 3) AS foo WHERE (a = 1)/,
+          q/SELECT * FROM (SELECT * FROM bar WHERE (b = 4)) AS foo WHERE a = 1/,
+          q/SELECT * FROM (SELECT * FROM bar WHERE (b = 5)) AS foo WHERE (a = 1)/,
+        ]
+      },
+      {
+        equal => 0,
+        statements => [
+          q/SELECT * FROM (SELECT * FROM bar WHERE b = 1 AND c = 1) AS foo WHERE a = 1/,
+          q/SELECT * FROM (SELECT * FROM bar WHERE (b = 1) AND c = 2) AS foo WHERE a = 1/,
+          q/SELECT * FROM (SELECT * FROM bar WHERE b = 1 AND (c = 3)) AS foo WHERE a = 1/,
+          q/SELECT * FROM (SELECT * FROM bar WHERE (b = 1) AND (c = 4)) AS foo WHERE a = 1/,
+          q/SELECT * FROM (SELECT * FROM bar WHERE ((b = 1) AND (c = 5))) AS foo WHERE a = 1/,
+
+          q/SELECT * FROM (SELECT * FROM bar WHERE b = 1 AND c = 6) AS foo WHERE (a = 1)/,
+          q/SELECT * FROM (SELECT * FROM bar WHERE (b = 1) AND c = 7) AS foo WHERE (a = 1)/,
+          q/SELECT * FROM (SELECT * FROM bar WHERE b = 1 AND (c = 8)) AS foo WHERE (a = 1)/,
+          q/SELECT * FROM (SELECT * FROM bar WHERE (b = 1) AND (c = 9)) AS foo WHERE (a = 1)/,
+          q/SELECT * FROM (SELECT * FROM bar WHERE ((b = 1) AND (c = 10))) AS foo WHERE (a = 1)/,
+
+          q/SELECT * FROM (SELECT * FROM bar WHERE b = 1 AND c = 1) AS foo WHERE a = 2/,
+          q/SELECT * FROM (SELECT * FROM bar WHERE (b = 1) AND c = 2) AS foo WHERE a = 2/,
+          q/SELECT * FROM (SELECT * FROM bar WHERE b = 1 AND (c = 3)) AS foo WHERE a = 2/,
+          q/SELECT * FROM (SELECT * FROM bar WHERE (b = 1) AND (c = 4)) AS foo WHERE a = 2/,
+          q/SELECT * FROM (SELECT * FROM bar WHERE ((b = 1) AND (c = 5))) AS foo WHERE a = 2/,
+
+          q/SELECT * FROM (SELECT * FROM bar WHERE b = 1 AND c = 6) AS foo WHERE (a = 2)/,
+          q/SELECT * FROM (SELECT * FROM bar WHERE (b = 1) AND c = 7) AS foo WHERE (a = 2)/,
+          q/SELECT * FROM (SELECT * FROM bar WHERE b = 1 AND (c = 8)) AS foo WHERE (a = 2)/,
+          q/SELECT * FROM (SELECT * FROM bar WHERE (b = 1) AND (c = 9)) AS foo WHERE (a = 2)/,
+          q/SELECT * FROM (SELECT * FROM bar WHERE ((b = 1) AND (c = 10))) AS foo WHERE (a = 2)/,
+        ]
+      },
+);
+
 my @bind_tests = (
   # scalar - equal
   {
@@ -200,14 +644,40 @@ my @bind_tests = (
   },
 );
 
-
-plan tests => 1 + sum
-  map { $_ * ($_ - 1) / 2 }
-    map { scalar @{$_->{bindvals}} }
-      @bind_tests;
+plan tests => 1 +
+  sum(
+    map { $_ * ($_ - 1) / 2 }
+      map { scalar @{$_->{statements}} }
+        @sql_tests
+  ) +
+  sum(
+    map { $_ * ($_ - 1) / 2 }
+      map { scalar @{$_->{bindvals}} }
+        @bind_tests
+  );
 
 use_ok('SQL::Abstract::Test', import => [qw(eq_sql eq_bind is_same_sql_bind)]);
 
+for my $test (@sql_tests) {
+  my $statements = $test->{statements};
+  while (@$statements) {
+    my $sql1 = shift @$statements;
+    foreach my $sql2 (@$statements) {
+      my $equal = eq_sql($sql1, $sql2);
+      if ($test->{equal}) {
+        ok($equal, "equal SQL expressions considered equal");
+      } else {
+        ok(!$equal, "different SQL expressions considered not equal");
+      }
+
+      if ($equal ^ $test->{equal}) {
+        diag("sql1: $sql1");
+        diag("sql2: $sql2");
+      }
+    }
+  }
+}
+
 for my $test (@bind_tests) {
   my $bindvals = $test->{bindvals};
   while (@$bindvals) {