[Bast-commits] r9684 - in SQL-Abstract/1.x/trunk: . lib/SQL/Abstract script t

ribasushi at dev.catalyst.perl.org ribasushi at dev.catalyst.perl.org
Mon Sep 6 14:42:17 GMT 2010


Author: ribasushi
Date: 2010-09-06 15:42:17 +0100 (Mon, 06 Sep 2010)
New Revision: 9684

Added:
   SQL-Abstract/1.x/trunk/Makefile.PL
   SQL-Abstract/1.x/trunk/lib/
   SQL-Abstract/1.x/trunk/lib/SQL/Abstract/Tree.pm
   SQL-Abstract/1.x/trunk/script/
   SQL-Abstract/1.x/trunk/script/format-sql
   SQL-Abstract/1.x/trunk/t/
   SQL-Abstract/1.x/trunk/t/11unparse.t
Removed:
   SQL-Abstract/1.x/trunk/Makefile.PL
   SQL-Abstract/1.x/trunk/lib/
   SQL-Abstract/1.x/trunk/t/
Modified:
   SQL-Abstract/1.x/trunk/
   SQL-Abstract/1.x/trunk/lib/SQL/Abstract/Test.pm
Log:
 r9676 at Thesaurus (orig r9662):  frew | 2010-08-31 05:31:56 +0200
 YASQLATBranch
 r9677 at Thesaurus (orig r9663):  frew | 2010-08-31 05:55:05 +0200
 break out Tree
 r9678 at Thesaurus (orig r9664):  frew | 2010-08-31 06:13:04 +0200
 document hopes and dreams
 r9679 at Thesaurus (orig r9665):  frew | 2010-09-01 05:55:49 +0200
 add format command
 r9680 at Thesaurus (orig r9666):  frew | 2010-09-01 06:13:03 +0200
 OO-ify and add some silly colors for fun
 r9683 at Thesaurus (orig r9669):  frew | 2010-09-02 05:08:36 +0200
 fix SQLATest, add more rudimentary formatting and some depth
 r9684 at Thesaurus (orig r9670):  frew | 2010-09-02 06:03:05 +0200
 rearrange things, looks almost good
 r9685 at Thesaurus (orig r9671):  frew | 2010-09-02 06:14:44 +0200
 good formatting for both subqueries and otherwise
 r9686 at Thesaurus (orig r9672):  frew | 2010-09-02 06:21:19 +0200
 do not indent for the first select
 r9687 at Thesaurus (orig r9673):  frew | 2010-09-02 06:40:03 +0200
 less warnings
 r9688 at Thesaurus (orig r9674):  frew | 2010-09-02 07:00:07 +0200
 initial profile and configuration support
 r9689 at Thesaurus (orig r9675):  frew | 2010-09-03 05:19:12 +0200
 sensible profiles and accessors for formatting
 r9690 at Thesaurus (orig r9676):  frew | 2010-09-04 03:12:46 +0200
 tests and slightly better profiles
 r9691 at Thesaurus (orig r9677):  frew | 2010-09-04 03:22:34 +0200
 test for no formatting
 r9692 at Thesaurus (orig r9678):  frew | 2010-09-04 04:05:47 +0200
 add html profile
 r9693 at Thesaurus (orig r9679):  frew | 2010-09-04 05:21:05 +0200
 add test for somewhat complex sql and add extra config for missing keywords
 r9694 at Thesaurus (orig r9680):  frew | 2010-09-04 05:31:51 +0200
 initial formatting script
 r9695 at Thesaurus (orig r9681):  frew | 2010-09-04 05:39:36 +0200
 add one more hope and dream (This should be easy to add)



Property changes on: SQL-Abstract/1.x/trunk
___________________________________________________________________
Modified: svk:merge
   - b9bda2dc-4395-4011-945f-8c81d782bde1:/branches/matthewt:18
b9bda2dc-4395-4011-945f-8c81d782bde1:/trunk:23
bd8105ee-0ff8-0310-8827-fb3f25b6796d:/SQL-Abstract/1.x/branches/and_or:6008
bd8105ee-0ff8-0310-8827-fb3f25b6796d:/SQL-Abstract/1.x/branches/arbitrary_op_nesting:8941
bd8105ee-0ff8-0310-8827-fb3f25b6796d:/SQL-Abstract/1.x/branches/bool_operator:7524
bd8105ee-0ff8-0310-8827-fb3f25b6796d:/SQL-Abstract/1.x/branches/special_op_handling:6158
bd8105ee-0ff8-0310-8827-fb3f25b6796d:/SQL-Abstract/1.x/branches/test_refactor:8533
bd8105ee-0ff8-0310-8827-fb3f25b6796d:/trunk/SQL-Abstract:3093
   + b9bda2dc-4395-4011-945f-8c81d782bde1:/branches/matthewt:18
b9bda2dc-4395-4011-945f-8c81d782bde1:/trunk:23
bd8105ee-0ff8-0310-8827-fb3f25b6796d:/SQL-Abstract/1.x/branches/and_or:6008
bd8105ee-0ff8-0310-8827-fb3f25b6796d:/SQL-Abstract/1.x/branches/arbitrary_op_nesting:8941
bd8105ee-0ff8-0310-8827-fb3f25b6796d:/SQL-Abstract/1.x/branches/bool_operator:7524
bd8105ee-0ff8-0310-8827-fb3f25b6796d:/SQL-Abstract/1.x/branches/special_op_handling:6158
bd8105ee-0ff8-0310-8827-fb3f25b6796d:/SQL-Abstract/1.x/branches/sqla-tree:9681
bd8105ee-0ff8-0310-8827-fb3f25b6796d:/SQL-Abstract/1.x/branches/test_refactor:8533
bd8105ee-0ff8-0310-8827-fb3f25b6796d:/trunk/SQL-Abstract:3093

Deleted: SQL-Abstract/1.x/trunk/Makefile.PL
===================================================================
--- SQL-Abstract/1.x/trunk/Makefile.PL	2010-09-06 14:39:22 UTC (rev 9683)
+++ SQL-Abstract/1.x/trunk/Makefile.PL	2010-09-06 14:42:17 UTC (rev 9684)
@@ -1,23 +0,0 @@
-use inc::Module::Install 1.0;
-use strict;
-use warnings;
-
-use 5.006002;
-
-perl_version '5.006002';
-name 'SQL-Abstract';
-author 'Nathan Wiger <nate at wiger.org>';
-
-all_from 'lib/SQL/Abstract.pm';
-
-requires 'List::Util'   => 0;
-requires 'Scalar::Util' => 0;
-
-test_requires "Test::More"      => 0;
-test_requires "Test::Exception" => 0;
-test_requires "Test::Warn"      => 0;
-test_requires "Storable"        => 0;   # for cloning in tests
-
-tests_recursive 't';
-
-WriteAll();

Copied: SQL-Abstract/1.x/trunk/Makefile.PL (from rev 9633, SQL-Abstract/1.x/trunk/Makefile.PL)
===================================================================
--- SQL-Abstract/1.x/trunk/Makefile.PL	                        (rev 0)
+++ SQL-Abstract/1.x/trunk/Makefile.PL	2010-09-06 14:42:17 UTC (rev 9684)
@@ -0,0 +1,23 @@
+use inc::Module::Install 1.0;
+use strict;
+use warnings;
+
+use 5.006002;
+
+perl_version '5.006002';
+name 'SQL-Abstract';
+author 'Nathan Wiger <nate at wiger.org>';
+
+all_from 'lib/SQL/Abstract.pm';
+
+requires 'List::Util'   => 0;
+requires 'Scalar::Util' => 0;
+
+test_requires "Test::More"      => 0.96;
+test_requires "Test::Exception" => 0;
+test_requires "Test::Warn"      => 0;
+test_requires "Storable"        => 0;   # for cloning in tests
+
+tests_recursive 't';
+
+WriteAll();

Modified: SQL-Abstract/1.x/trunk/lib/SQL/Abstract/Test.pm
===================================================================
--- SQL-Abstract/1.x/trunk/lib/SQL/Abstract/Test.pm	2010-06-29 14:09:36 UTC (rev 9615)
+++ SQL-Abstract/1.x/trunk/lib/SQL/Abstract/Test.pm	2010-09-06 14:42:17 UTC (rev 9684)
@@ -4,82 +4,20 @@
 use warnings;
 use base qw/Test::Builder::Module Exporter/;
 use Data::Dumper;
-use Carp;
 use Test::Builder;
+use SQL::Abstract::Tree;
 
 our @EXPORT_OK = qw/&is_same_sql_bind &is_same_sql &is_same_bind
-                    &eq_sql_bind &eq_sql &eq_bind 
+                    &eq_sql_bind &eq_sql &eq_bind
                     $case_sensitive $sql_differ/;
 
+my $sqlat = SQL::Abstract::Tree->new;
+
 our $case_sensitive = 0;
 our $parenthesis_significant = 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;
-use constant PARSE_IN_EXPR => 1;
-use constant PARSE_IN_PARENS => 2;
-use constant PARSE_RHS => 3;
-
-# 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 = (
-  'SELECT',
-  'FROM',
-  '(?:
-    (?:
-        (?: \b (?: LEFT | RIGHT | FULL ) \s+ )?
-        (?: \b (?: CROSS | INNER | OUTER ) \s+ )?
-    )?
-    JOIN
-  )',
-  'ON',
-  'WHERE',
-  'EXISTS',
-  'GROUP \s+ BY',
-  'HAVING',
-  'ORDER \s+ BY',
-  'LIMIT',
-  'OFFSET',
-  'FOR',
-  'UNION',
-  'INTERSECT',
-  'EXCEPT',
-  'RETURNING',
-);
-
-# These are binary operator keywords always a single LHS and RHS
-# * AND/OR are handled separately as they are N-ary
-# * so is NOT as being unary
-# * BETWEEN without paranthesis around the ANDed arguments (which
-#   makes it a non-binary op) is detected and accomodated in 
-#   _recurse_parse()
-my $stuff_around_mathops = qr/[\w\s\`\'\"\)]/;
-my @binary_op_keywords = (
-  ( map
-    {
-      ' ^ '  . quotemeta ($_) . "(?= \$ | $stuff_around_mathops ) ",
-      " (?<= $stuff_around_mathops)" . quotemeta ($_) . "(?= \$ | $stuff_around_mathops ) ",
-    }
-    (qw/< > != <> = <= >=/)
-  ),
-  ( map
-    { '\b (?: NOT \s+)?' . $_ . '\b' }
-    (qw/IN BETWEEN LIKE/)
-  ),
-);
-
-my $tokenizer_re_str = join("\n\t|\n",
-  ( map { '\b' . $_ . '\b' } @expression_terminator_sql_keywords, 'AND', 'OR', 'NOT'),
-  @binary_op_keywords,
-);
-
-my $tokenizer_re = qr/ \s* ( $tokenizer_re_str | \( | \) | \? ) \s* /xi;
-
 # All of these keywords allow their parameters to be specified with or without parenthesis without changing the semantics
 my @unrollable_ops = (
   'ON',
@@ -186,8 +124,8 @@
   my ($sql1, $sql2) = @_;
 
   # parse
-  my $tree1 = parse($sql1);
-  my $tree2 = parse($sql2);
+  my $tree1 = $sqlat->parse($sql1);
+  my $tree2 = $sqlat->parse($sql2);
 
   return 1 if _eq_sql($tree1, $tree2);
 }
@@ -205,7 +143,7 @@
   }
   # one is a list, the other is an op with a list
   elsif (ref $left->[0] xor ref $right->[0]) {
-    $sql_differ = sprintf ("left: %s\nright: %s\n", map { unparse ($_) } ($left, $right) );
+    $sql_differ = sprintf ("left: %s\nright: %s\n", map { $sqlat->unparse ($_) } ($left, $right) );
     return 0;
   }
   # one is a list, so is the other
@@ -224,12 +162,12 @@
     # if operators are different
     if ( $left->[0] ne $right->[0] ) {
       $sql_differ = sprintf "OP [$left->[0]] != [$right->[0]] in\nleft: %s\nright: %s\n",
-        unparse($left),
-        unparse($right);
+        $sqlat->unparse($left),
+        $sqlat->unparse($right);
       return 0;
     }
     # elsif operators are identical, compare operands
-    else { 
+    else {
       if ($left->[0] eq 'LITERAL' ) { # unary
         (my $l = " $left->[1][0] " ) =~ s/\s+/ /g;
         (my $r = " $right->[1][0] ") =~ s/\s+/ /g;
@@ -239,106 +177,13 @@
       }
       else {
         my $eq = _eq_sql($left->[1], $right->[1]);
-        $sql_differ ||= sprintf ("left: %s\nright: %s\n", map { unparse ($_) } ($left, $right) ) if not $eq;
+        $sql_differ ||= sprintf ("left: %s\nright: %s\n", map { $sqlat->unparse ($_) } ($left, $right) ) if not $eq;
         return $eq;
       }
     }
   }
 }
 
-sub parse {
-  my $s = shift;
-
-  # tokenize string, and remove all optional whitespace
-  my $tokens = [];
-  foreach my $token (split $tokenizer_re, $s) {
-    push @$tokens, $token if (length $token) && ($token =~ /\S/);
-  }
-
-  my $tree = _recurse_parse($tokens, PARSE_TOP_LEVEL);
-  return $tree;
-}
-
-sub _recurse_parse {
-  my ($tokens, $state) = @_;
-
-  my $left;
-  while (1) { # left-associative parsing
-
-    my $lookahead = $tokens->[0];
-    if ( not defined($lookahead)
-          or
-        ($state == PARSE_IN_PARENS && $lookahead eq ')')
-          or
-        ($state == PARSE_IN_EXPR && grep { $lookahead =~ /^ $_ $/xi } ('\)', @expression_terminator_sql_keywords ) )
-          or
-        ($state == PARSE_RHS && grep { $lookahead =~ /^ $_ $/xi } ('\)', @expression_terminator_sql_keywords, @binary_op_keywords, 'AND', 'OR', 'NOT' ) )
-    ) {
-      return $left;
-    }
-
-    my $token = shift @$tokens;
-
-    # nested expression in ()
-    if ($token eq '(' ) {
-      my $right = _recurse_parse($tokens, PARSE_IN_PARENS);
-      $token = shift @$tokens   or croak "missing closing ')' around block " . unparse ($right);
-      $token eq ')'             or croak "unexpected token '$token' terminating block " . unparse ($right);
-
-      $left = $left ? [@$left, [PAREN => [$right] ]]
-                    : [PAREN  => [$right] ];
-    }
-    # AND/OR
-    elsif ($token =~ /^ (?: OR | AND ) $/xi )  {
-      my $op = uc $token;
-      my $right = _recurse_parse($tokens, PARSE_IN_EXPR);
-
-      # Merge chunks if logic matches
-      if (ref $right and $op eq $right->[0]) {
-        $left = [ (shift @$right ), [$left, map { @$_ } @$right] ];
-      }
-      else {
-       $left = [$op => [$left, $right]];
-      }
-    }
-    # binary operator keywords
-    elsif (grep { $token =~ /^ $_ $/xi } @binary_op_keywords ) {
-      my $op = uc $token;
-      my $right = _recurse_parse($tokens, PARSE_RHS);
-
-      # A between with a simple LITERAL for a 1st RHS argument needs a
-      # rerun of the search to (hopefully) find the proper AND construct
-      if ($op eq 'BETWEEN' and $right->[0] eq 'LITERAL') {
-        unshift @$tokens, $right->[1][0];
-        $right = _recurse_parse($tokens, PARSE_IN_EXPR);
-      }
-
-      $left = [$op => [$left, $right] ];
-    }
-    # expression terminator keywords (as they start a new expression)
-    elsif (grep { $token =~ /^ $_ $/xi } @expression_terminator_sql_keywords ) {
-      my $op = uc $token;
-      my $right = _recurse_parse($tokens, PARSE_IN_EXPR);
-      $left = $left ? [ $left,  [$op => [$right] ]]
-                    : [ $op => [$right] ];
-    }
-    # NOT (last as to allow all other NOT X pieces first)
-    elsif ( $token =~ /^ not $/ix ) {
-      my $op = uc $token;
-      my $right = _recurse_parse ($tokens, PARSE_RHS);
-      $left = $left ? [ @$left, [$op => [$right] ]]
-                    : [ $op => [$right] ];
-
-    }
-    # literal (eat everything on the right until RHS termination)
-    else {
-      my $right = _recurse_parse ($tokens, PARSE_RHS);
-      $left = $left ? [ $left, [LITERAL => [join ' ', $token, unparse($right)||()] ] ]
-                    : [ LITERAL => [join ' ', $token, unparse($right)||()] ];
-    }
-  }
-}
-
 sub _parenthesis_unroll {
   my $ast = shift;
 
@@ -390,7 +235,7 @@
       elsif (
         @{$child->[1]} == 1
           and
-        grep { $child->[1][0][0] =~ /^ $_ $/xi } (@binary_op_keywords)
+        grep { $child->[1][0][0] =~ /^ $_ $/xi } (SQL::Abstract::Tree::_binary_op_keywords())
           and
         $child->[1][0][1][0][0] eq 'LITERAL'
           and
@@ -412,30 +257,6 @@
 
 }
 
-sub unparse {
-  my $tree = shift;
-
-  if (not $tree ) {
-    return '';
-  }
-  elsif (ref $tree->[0]) {
-    return join (" ", map { unparse ($_) } @$tree);
-  }
-  elsif ($tree->[0] eq 'LITERAL') {
-    return $tree->[1][0];
-  }
-  elsif ($tree->[0] eq 'PAREN') {
-    return sprintf '(%s)', join (" ", map {unparse($_)} @{$tree->[1]});
-  }
-  elsif ($tree->[0] eq 'OR' or $tree->[0] eq 'AND' or (grep { $tree->[0] =~ /^ $_ $/xi } @binary_op_keywords ) ) {
-    return join (" $tree->[0] ", map {unparse($_)} @{$tree->[1]});
-  }
-  else {
-    return sprintf '%s %s', $tree->[0], unparse ($tree->[1]);
-  }
-}
-
-
 1;
 
 
@@ -456,13 +277,13 @@
 
   my ($sql, @bind) = SQL::Abstract->new->select(%args);
 
-  is_same_sql_bind($given_sql,    \@given_bind, 
+  is_same_sql_bind($given_sql,    \@given_bind,
                    $expected_sql, \@expected_bind, $test_msg);
 
   is_same_sql($given_sql, $expected_sql, $test_msg);
   is_same_bind(\@given_bind, \@expected_bind, $test_msg);
 
-  my $is_same = eq_sql_bind($given_sql,    \@given_bind, 
+  my $is_same = eq_sql_bind($given_sql,    \@given_bind,
                             $expected_sql, \@expected_bind);
 
   my $sql_same = eq_sql($given_sql, $expected_sql);
@@ -486,14 +307,14 @@
 Currently this module does not support commutativity and more
 intelligent transformations like Morgan laws, etc.
 
-For a good overview of what this test framework is capable of refer 
+For a good overview of what this test framework is capable of refer
 to C<t/10test.t>
 
 =head1 FUNCTIONS
 
 =head2 is_same_sql_bind
 
-  is_same_sql_bind($given_sql,    \@given_bind, 
+  is_same_sql_bind($given_sql,    \@given_bind,
                    $expected_sql, \@expected_bind, $test_msg);
 
 Compares given and expected pairs of C<($sql, \@bind)>, and calls
@@ -524,7 +345,7 @@
 
 =head2 eq_sql_bind
 
-  my $is_same = eq_sql_bind($given_sql,    \@given_bind, 
+  my $is_same = eq_sql_bind($given_sql,    \@given_bind,
                             $expected_sql, \@expected_bind);
 
 Compares given and expected pairs of C<($sql, \@bind)>. Similar to
@@ -585,4 +406,4 @@
 Copyright 2008 by Laurent Dami.
 
 This library is free software; you can redistribute it and/or modify
-it under the same terms as Perl itself. 
+it under the same terms as Perl itself.

Added: SQL-Abstract/1.x/trunk/lib/SQL/Abstract/Tree.pm
===================================================================
--- SQL-Abstract/1.x/trunk/lib/SQL/Abstract/Tree.pm	                        (rev 0)
+++ SQL-Abstract/1.x/trunk/lib/SQL/Abstract/Tree.pm	2010-09-06 14:42:17 UTC (rev 9684)
@@ -0,0 +1,314 @@
+package SQL::Abstract::Tree;
+
+use strict;
+use warnings;
+use Carp;
+
+
+use base 'Class::Accessor::Grouped';
+
+__PACKAGE__->mk_group_accessors( simple => $_ ) for qw(
+   newline indent_string indent_amount colormap indentmap
+);
+
+# Parser states for _recurse_parse()
+use constant PARSE_TOP_LEVEL => 0;
+use constant PARSE_IN_EXPR => 1;
+use constant PARSE_IN_PARENS => 2;
+use constant PARSE_RHS => 3;
+
+# 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 = (
+  'SELECT',
+  'FROM',
+  '(?:
+    (?:
+        (?: \b (?: LEFT | RIGHT | FULL ) \s+ )?
+        (?: \b (?: CROSS | INNER | OUTER ) \s+ )?
+    )?
+    JOIN
+  )',
+  'ON',
+  'WHERE',
+  'EXISTS',
+  'GROUP \s+ BY',
+  'HAVING',
+  'ORDER \s+ BY',
+  'LIMIT',
+  'OFFSET',
+  'FOR',
+  'UNION',
+  'INTERSECT',
+  'EXCEPT',
+  'RETURNING',
+);
+
+# These are binary operator keywords always a single LHS and RHS
+# * AND/OR are handled separately as they are N-ary
+# * so is NOT as being unary
+# * BETWEEN without paranthesis around the ANDed arguments (which
+#   makes it a non-binary op) is detected and accomodated in
+#   _recurse_parse()
+my $stuff_around_mathops = qr/[\w\s\`\'\"\)]/;
+my @binary_op_keywords = (
+  ( map
+    {
+      ' ^ '  . quotemeta ($_) . "(?= \$ | $stuff_around_mathops ) ",
+      " (?<= $stuff_around_mathops)" . quotemeta ($_) . "(?= \$ | $stuff_around_mathops ) ",
+    }
+    (qw/< > != <> = <= >=/)
+  ),
+  ( map
+    { '\b (?: NOT \s+)?' . $_ . '\b' }
+    (qw/IN BETWEEN LIKE/)
+  ),
+);
+
+my $tokenizer_re_str = join("\n\t|\n",
+  ( map { '\b' . $_ . '\b' } @expression_terminator_sql_keywords, 'AND', 'OR', 'NOT'),
+  @binary_op_keywords,
+);
+
+my $tokenizer_re = qr/ \s* ( $tokenizer_re_str | \( | \) | \? ) \s* /xi;
+
+sub _binary_op_keywords { @binary_op_keywords }
+
+my %indents = (
+   select     => 0,
+   where      => 1,
+   from       => 1,
+   join       => 1,
+   on         => 2,
+   'group by' => 1,
+);
+
+my %profiles = (
+   console => {
+      indent_string => ' ',
+      indent_amount => 2,
+      newline       => "\n",
+      colormap      => {},
+      indentmap     => { %indents },
+   },
+   console_monochrome => {
+      indent_string => ' ',
+      indent_amount => 2,
+      newline       => "\n",
+      colormap      => {},
+      indentmap     => { %indents },
+   },
+   html => {
+      indent_string => '&nbsp;',
+      indent_amount => 2,
+      newline       => "<br />\n",
+      colormap      => {
+         select     => ['<span class="select">'  , '</span>'],
+         where      => ['<span class="where">'   , '</span>'],
+         from       => ['<span class="from">'    , '</span>'],
+         join       => ['<span class="join">'    , '</span>'],
+         on         => ['<span class="on">'      , '</span>'],
+         'group by' => ['<span class="group-by">', '</span>'],
+      },
+      indentmap     => { %indents },
+   },
+   none => {
+      colormap      => {},
+      indentmap     => {},
+   },
+);
+
+eval {
+   require Term::ANSIColor;
+   $profiles{console}->{colormap} = {
+      select => [Term::ANSIColor::color('red'), Term::ANSIColor::color('reset')],
+      where  => [Term::ANSIColor::color('green'), Term::ANSIColor::color('reset')],
+      from   => [Term::ANSIColor::color('cyan'), Term::ANSIColor::color('reset')],
+   };
+};
+
+sub new {
+   my ($class, $args) = @_;
+
+   my $profile = delete $args->{profile} || 'none';
+   my $data = {%{$profiles{$profile}}, %{$args||{}}};
+
+   bless $data, $class
+}
+
+sub parse {
+  my ($self, $s) = @_;
+
+  # tokenize string, and remove all optional whitespace
+  my $tokens = [];
+  foreach my $token (split $tokenizer_re, $s) {
+    push @$tokens, $token if (length $token) && ($token =~ /\S/);
+  }
+
+  my $tree = $self->_recurse_parse($tokens, PARSE_TOP_LEVEL);
+  return $tree;
+}
+
+sub _recurse_parse {
+  my ($self, $tokens, $state) = @_;
+
+  my $left;
+  while (1) { # left-associative parsing
+
+    my $lookahead = $tokens->[0];
+    if ( not defined($lookahead)
+          or
+        ($state == PARSE_IN_PARENS && $lookahead eq ')')
+          or
+        ($state == PARSE_IN_EXPR && grep { $lookahead =~ /^ $_ $/xi } ('\)', @expression_terminator_sql_keywords ) )
+          or
+        ($state == PARSE_RHS && grep { $lookahead =~ /^ $_ $/xi } ('\)', @expression_terminator_sql_keywords, @binary_op_keywords, 'AND', 'OR', 'NOT' ) )
+    ) {
+      return $left;
+    }
+
+    my $token = shift @$tokens;
+
+    # nested expression in ()
+    if ($token eq '(' ) {
+      my $right = $self->_recurse_parse($tokens, PARSE_IN_PARENS);
+      $token = shift @$tokens   or croak "missing closing ')' around block " . $self->unparse($right);
+      $token eq ')'             or croak "unexpected token '$token' terminating block " . $self->unparse($right);
+
+      $left = $left ? [@$left, [PAREN => [$right] ]]
+                    : [PAREN  => [$right] ];
+    }
+    # AND/OR
+    elsif ($token =~ /^ (?: OR | AND ) $/xi )  {
+      my $op = uc $token;
+      my $right = $self->_recurse_parse($tokens, PARSE_IN_EXPR);
+
+      # Merge chunks if logic matches
+      if (ref $right and $op eq $right->[0]) {
+        $left = [ (shift @$right ), [$left, map { @$_ } @$right] ];
+      }
+      else {
+       $left = [$op => [$left, $right]];
+      }
+    }
+    # binary operator keywords
+    elsif (grep { $token =~ /^ $_ $/xi } @binary_op_keywords ) {
+      my $op = uc $token;
+      my $right = $self->_recurse_parse($tokens, PARSE_RHS);
+
+      # A between with a simple LITERAL for a 1st RHS argument needs a
+      # rerun of the search to (hopefully) find the proper AND construct
+      if ($op eq 'BETWEEN' and $right->[0] eq 'LITERAL') {
+        unshift @$tokens, $right->[1][0];
+        $right = $self->_recurse_parse($tokens, PARSE_IN_EXPR);
+      }
+
+      $left = [$op => [$left, $right] ];
+    }
+    # expression terminator keywords (as they start a new expression)
+    elsif (grep { $token =~ /^ $_ $/xi } @expression_terminator_sql_keywords ) {
+      my $op = uc $token;
+      my $right = $self->_recurse_parse($tokens, PARSE_IN_EXPR);
+      $left = $left ? [ $left,  [$op => [$right] ]]
+                    : [ $op => [$right] ];
+    }
+    # NOT (last as to allow all other NOT X pieces first)
+    elsif ( $token =~ /^ not $/ix ) {
+      my $op = uc $token;
+      my $right = $self->_recurse_parse ($tokens, PARSE_RHS);
+      $left = $left ? [ @$left, [$op => [$right] ]]
+                    : [ $op => [$right] ];
+
+    }
+    # literal (eat everything on the right until RHS termination)
+    else {
+      my $right = $self->_recurse_parse ($tokens, PARSE_RHS);
+      $left = $left ? [ $left, [LITERAL => [join ' ', $token, $self->unparse($right)||()] ] ]
+                    : [ LITERAL => [join ' ', $token, $self->unparse($right)||()] ];
+    }
+  }
+}
+
+sub format_keyword {
+  my ($self, $keyword) = @_;
+
+  if (my $around = $self->colormap->{lc $keyword}) {
+     $keyword = "$around->[0]$keyword$around->[1]";
+  }
+
+  return $keyword
+}
+
+sub whitespace {
+   my ($self, $keyword, $depth) = @_;
+
+   my $before = '';
+   my $after  = ' ';
+   if (defined $self->indentmap->{lc $keyword}) {
+      $before = $self->newline . $self->indent($depth + $self->indentmap->{lc $keyword});
+   }
+   $before = '' if $depth == 0 and lc $keyword eq 'select';
+   return [$before, $after];
+}
+
+sub indent { ($_[0]->indent_string||'') x ( ( $_[0]->indent_amount || 0 ) * $_[1] ) }
+
+sub _is_select {
+   my $tree = shift;
+   $tree = $tree->[0] while ref $tree;
+
+   defined $tree && lc $tree eq 'select';
+}
+
+sub unparse {
+  my ($self, $tree, $depth) = @_;
+
+  $depth ||= 0;
+
+  if (not $tree ) {
+    return '';
+  }
+
+  my $car = $tree->[0];
+  my $cdr = $tree->[1];
+
+  if (ref $car) {
+    return join ('', map $self->unparse($_, $depth), @$tree);
+  }
+  elsif ($car eq 'LITERAL') {
+    return $cdr->[0];
+  }
+  elsif ($car eq 'PAREN') {
+    return '(' .
+      join(' ',
+        map $self->unparse($_, $depth + 2), @{$cdr}) .
+    (_is_select($cdr)?( $self->newline||'' ).$self->indent($depth + 1):'') . ')';
+  }
+  elsif ($car eq 'OR' or $car eq 'AND' or (grep { $car =~ /^ $_ $/xi } @binary_op_keywords ) ) {
+    return join (" $car ", map $self->unparse($_, $depth), @{$cdr});
+  }
+  else {
+    my ($l, $r) = @{$self->whitespace($car, $depth)};
+    return sprintf "$l%s %s$r", $self->format_keyword($car), $self->unparse($cdr, $depth);
+  }
+}
+
+sub format { my $self = shift; $self->unparse($self->parse(@_)) }
+
+1;
+
+=pod
+
+=head1 SYNOPSIS
+
+ my $sqla_tree = SQL::Abstract::Tree->new({ profile => 'console' });
+
+ print $sqla_tree->format('SELECT * FROM foo WHERE foo.a > 2');
+
+ # SELECT *
+ #   FROM foo
+ #   WHERE foo.a > 2
+

Added: SQL-Abstract/1.x/trunk/script/format-sql
===================================================================
--- SQL-Abstract/1.x/trunk/script/format-sql	                        (rev 0)
+++ SQL-Abstract/1.x/trunk/script/format-sql	2010-09-06 14:42:17 UTC (rev 9684)
@@ -0,0 +1,7 @@
+#!/usr/bin/env perl
+
+use SQL::Abstract::Tree;
+
+my $sqlat = SQL::Abstract::Tree->new({ profile => 'console' });
+
+print $sqlat->format($_) . "\n" while <>;


Property changes on: SQL-Abstract/1.x/trunk/script/format-sql
___________________________________________________________________
Added: svn:executable
   + *

Added: SQL-Abstract/1.x/trunk/t/11unparse.t
===================================================================
--- SQL-Abstract/1.x/trunk/t/11unparse.t	                        (rev 0)
+++ SQL-Abstract/1.x/trunk/t/11unparse.t	2010-09-06 14:42:17 UTC (rev 9684)
@@ -0,0 +1,257 @@
+use strict;
+use warnings;
+
+use Test::More;
+use SQL::Abstract::Tree;
+
+subtest no_formatting => sub {
+   my $sqlat = SQL::Abstract::Tree->new;
+
+   {
+      my $sql = "SELECT a, b, c FROM foo WHERE foo.a =1 and foo.b LIKE 'station'";
+      my $expected_sql =
+         "SELECT a, b, c FROM foo WHERE foo.a = 1 AND foo.b LIKE 'station' ";
+      is($sqlat->format($sql), $expected_sql,
+         'simple statement formatted correctly'
+      );
+   }
+
+   {
+      my $sql = "SELECT * FROM (SELECT * FROM foobar) WHERE foo.a =1 and foo.b LIKE 'station'";
+      my $expected_sql =
+         "SELECT * FROM (SELECT * FROM foobar ) WHERE foo.a = 1 AND foo.b LIKE 'station' ";
+      is($sqlat->format($sql), $expected_sql,
+         'subquery statement formatted correctly'
+      );
+   }
+
+   {
+      my $sql = "SELECT * FROM lolz WHERE ( foo.a =1 ) and foo.b LIKE 'station'";
+      my $expected_sql =
+         "SELECT * FROM lolz WHERE (foo.a = 1) AND foo.b LIKE 'station' ";
+
+      is($sqlat->format($sql), $expected_sql,
+         'simple statement with parens in where formatted correctly'
+      );
+   }
+
+   {
+      my $sql = "SELECT [screen].[id], [screen].[name], [screen].[section_id], [screen].[xtype] FROM [users_roles] [me] JOIN [roles] [role] ON [role].[id] = [me].[role_id] JOIN [roles_permissions] [role_permissions] ON [role_permissions].[role_id] = [role].[id] JOIN [permissions] [permission] ON [permission].[id] = [role_permissions].[permission_id] JOIN [permissionscreens] [permission_screens] ON [permission_screens].[permission_id] = [permission].[id] JOIN [screens] [screen] ON [screen].[id] = [permission_screens].[screen_id] WHERE ( [me].[user_id] = ? ) GROUP BY [screen].[id], [screen].[name], [screen].[section_id], [screen].[xtype]";
+      my $expected_sql =
+         "SELECT [screen].[id], [screen].[name], [screen].[section_id], [screen].[xtype] FROM [users_roles] [me] JOIN [roles] [role] ON [role].[id] = [me].[role_id] JOIN [roles_permissions] [role_permissions] ON [role_permissions].[role_id] = [role].[id] JOIN [permissions] [permission] ON [permission].[id] = [role_permissions].[permission_id] JOIN [permissionscreens] [permission_screens] ON [permission_screens].[permission_id] = [permission].[id] JOIN [screens] [screen] ON [screen].[id] = [permission_screens].[screen_id] WHERE ([me].[user_id] = ?) GROUP BY [screen].[id], [screen].[name], [screen].[section_id], [screen].[xtype] ";
+
+      is($sqlat->format($sql), $expected_sql,
+         'real life statement 1 formatted correctly'
+      );
+   }
+   done_testing;
+};
+
+subtest console_monochrome => sub {
+   my $sqlat = SQL::Abstract::Tree->new({
+      profile => 'console_monochrome',
+   });
+
+   {
+      my $sql = "SELECT a, b, c FROM foo WHERE foo.a =1 and foo.b LIKE 'station'";
+      my $expected_sql =
+         qq{SELECT a, b, c \n} .
+         qq{  FROM foo \n} .
+         qq{  WHERE foo.a = 1 AND foo.b LIKE 'station' };
+      is($sqlat->format($sql), $expected_sql,
+         'simple statement formatted correctly'
+      );
+   }
+
+   {
+      my $sql = "SELECT * FROM (SELECT * FROM foobar) WHERE foo.a =1 and foo.b LIKE 'station'";
+      my $expected_sql =
+         qq{SELECT * \n} .
+         qq{  FROM (\n} .
+         qq{    SELECT * \n} .
+         qq{      FROM foobar \n} .
+         qq{  ) \n} .
+         qq{  WHERE foo.a = 1 AND foo.b LIKE 'station' };
+
+      is($sqlat->format($sql), $expected_sql,
+         'subquery statement formatted correctly'
+      );
+   }
+
+   {
+      my $sql = "SELECT * FROM lolz WHERE ( foo.a =1 ) and foo.b LIKE 'station'";
+      my $expected_sql =
+         qq{SELECT * \n} .
+         qq{  FROM lolz \n} .
+         qq{  WHERE (foo.a = 1) AND foo.b LIKE 'station' };
+
+      is($sqlat->format($sql), $expected_sql,
+         'simple statement with parens in where formatted correctly'
+      );
+   }
+
+   {
+      my $sql = "SELECT [screen].[id], [screen].[name], [screen].[section_id], [screen].[xtype] FROM [users_roles] [me] JOIN [roles] [role] ON [role].[id] = [me].[role_id] JOIN [roles_permissions] [role_permissions] ON [role_permissions].[role_id] = [role].[id] JOIN [permissions] [permission] ON [permission].[id] = [role_permissions].[permission_id] JOIN [permissionscreens] [permission_screens] ON [permission_screens].[permission_id] = [permission].[id] JOIN [screens] [screen] ON [screen].[id] = [permission_screens].[screen_id] WHERE ( [me].[user_id] = ? ) GROUP BY [screen].[id], [screen].[name], [screen].[section_id], [screen].[xtype]";
+      my $expected_sql =
+         "SELECT [screen].[id], [screen].[name], [screen].[section_id], [screen].[xtype] \n".
+         "  FROM [users_roles] [me] \n" .
+         "  JOIN [roles] [role] \n" .
+         "    ON [role].[id] = [me].[role_id] \n" .
+         "  JOIN [roles_permissions] [role_permissions] \n" .
+         "    ON [role_permissions].[role_id] = [role].[id] \n" .
+         "  JOIN [permissions] [permission] \n" .
+         "    ON [permission].[id] = [role_permissions].[permission_id] \n" .
+         "  JOIN [permissionscreens] [permission_screens] \n" .
+         "    ON [permission_screens].[permission_id] = [permission].[id] \n" .
+         "  JOIN [screens] [screen] \n" .
+         "    ON [screen].[id] = [permission_screens].[screen_id] \n" .
+         "  WHERE ([me].[user_id] = ?) \n" .
+         "  GROUP BY [screen].[id], [screen].[name], [screen].[section_id], [screen].[xtype] ";
+
+      my $gotten = $sqlat->format($sql);
+      is($gotten, $expected_sql, 'real life statement 1 formatted correctly');
+   }
+   done_testing;
+};
+
+subtest html => sub {
+   my $sqlat = SQL::Abstract::Tree->new({
+      profile => 'html',
+   });
+
+   {
+      my $sql = "SELECT a, b, c FROM foo WHERE foo.a =1 and foo.b LIKE 'station'";
+      my $expected_sql =
+         qq{<span class="select">SELECT</span> a, b, c <br />\n} .
+         qq{&nbsp;&nbsp;<span class="from">FROM</span> foo <br />\n} .
+         qq{&nbsp;&nbsp;<span class="where">WHERE</span> foo.a = 1 AND foo.b LIKE 'station' };
+      is($sqlat->format($sql), $expected_sql,
+         'simple statement formatted correctly'
+      );
+   }
+
+   {
+      my $sql = "SELECT * FROM (SELECT * FROM foobar) WHERE foo.a =1 and foo.b LIKE 'station'";
+      my $expected_sql =
+         qq{<span class="select">SELECT</span> * <br />\n} .
+         qq{&nbsp;&nbsp;<span class="from">FROM</span> (<br />\n} .
+         qq{&nbsp;&nbsp;&nbsp;&nbsp;<span class="select">SELECT</span> * <br />\n} .
+         qq{&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="from">FROM</span> foobar <br />\n} .
+         qq{&nbsp;&nbsp;) <br />\n} .
+         qq{&nbsp;&nbsp;<span class="where">WHERE</span> foo.a = 1 AND foo.b LIKE 'station' };
+
+      is($sqlat->format($sql), $expected_sql,
+         'subquery statement formatted correctly'
+      );
+   }
+
+   {
+      my $sql = "SELECT * FROM lolz WHERE ( foo.a =1 ) and foo.b LIKE 'station'";
+      my $expected_sql =
+         qq{<span class="select">SELECT</span> * <br />\n} .
+         qq{&nbsp;&nbsp;<span class="from">FROM</span> lolz <br />\n} .
+         qq{&nbsp;&nbsp;<span class="where">WHERE</span> (foo.a = 1) AND foo.b LIKE 'station' };
+
+      is($sqlat->format($sql), $expected_sql,
+         'simple statement with parens in where formatted correctly'
+      );
+   }
+
+   {
+      my $sql = "SELECT [screen].[id], [screen].[name], [screen].[section_id], [screen].[xtype] FROM [users_roles] [me] JOIN [roles] [role] ON [role].[id] = [me].[role_id] JOIN [roles_permissions] [role_permissions] ON [role_permissions].[role_id] = [role].[id] JOIN [permissions] [permission] ON [permission].[id] = [role_permissions].[permission_id] JOIN [permissionscreens] [permission_screens] ON [permission_screens].[permission_id] = [permission].[id] JOIN [screens] [screen] ON [screen].[id] = [permission_screens].[screen_id] WHERE ( [me].[user_id] = ? ) GROUP BY [screen].[id], [screen].[name], [screen].[section_id], [screen].[xtype]";
+      my $expected_sql =
+         qq{<span class="select">SELECT</span> [screen].[id], [screen].[name], [screen].[section_id], [screen].[xtype] <br />\n}.
+         qq{&nbsp;&nbsp;<span class="from">FROM</span> [users_roles] [me] <br />\n}.
+         qq{&nbsp;&nbsp;<span class="join">JOIN</span> [roles] [role] <br />\n}.
+         qq{&nbsp;&nbsp;&nbsp;&nbsp;<span class="on">ON</span> [role].[id] = [me].[role_id] <br />\n}.
+         qq{&nbsp;&nbsp;<span class="join">JOIN</span> [roles_permissions] [role_permissions] <br />\n}.
+         qq{&nbsp;&nbsp;&nbsp;&nbsp;<span class="on">ON</span> [role_permissions].[role_id] = [role].[id] <br />\n}.
+         qq{&nbsp;&nbsp;<span class="join">JOIN</span> [permissions] [permission] <br />\n}.
+         qq{&nbsp;&nbsp;&nbsp;&nbsp;<span class="on">ON</span> [permission].[id] = [role_permissions].[permission_id] <br />\n}.
+         qq{&nbsp;&nbsp;<span class="join">JOIN</span> [permissionscreens] [permission_screens] <br />\n}.
+         qq{&nbsp;&nbsp;&nbsp;&nbsp;<span class="on">ON</span> [permission_screens].[permission_id] = [permission].[id] <br />\n}.
+         qq{&nbsp;&nbsp;<span class="join">JOIN</span> [screens] [screen] <br />\n}.
+         qq{&nbsp;&nbsp;&nbsp;&nbsp;<span class="on">ON</span> [screen].[id] = [permission_screens].[screen_id] <br />\n}.
+         qq{&nbsp;&nbsp;<span class="where">WHERE</span> ([me].[user_id] = ?) <br />\n}.
+         qq{&nbsp;&nbsp;<span class="group-by">GROUP BY</span> [screen].[id], [screen].[name], [screen].[section_id], [screen].[xtype] };
+
+      my $gotten = $sqlat->format($sql);
+      is($gotten, $expected_sql, 'real life statement 1 formatted correctly');
+   }
+   done_testing;
+};
+
+subtest configuration => sub {
+   my $sqlat = SQL::Abstract::Tree->new({
+      profile => 'console_monochrome',
+      indent_string => "\t",
+      indent_amount => 1,
+      newline => "\r\n",
+   });
+
+   {
+      my $sql = "SELECT a, b, c FROM foo WHERE foo.a =1 and foo.b LIKE 'station'";
+      my $expected_sql =
+         qq{SELECT a, b, c \r\n} .
+         qq{\tFROM foo \r\n} .
+         qq{\tWHERE foo.a = 1 AND foo.b LIKE 'station' };
+      is($sqlat->format($sql), $expected_sql,
+         'simple statement formatted correctly'
+      );
+   }
+
+   {
+      my $sql = "SELECT * FROM (SELECT * FROM foobar) WHERE foo.a =1 and foo.b LIKE 'station'";
+      my $expected_sql =
+         qq{SELECT * \r\n} .
+         qq{\tFROM (\r\n} .
+         qq{\t\tSELECT * \r\n} .
+         qq{\t\t\tFROM foobar \r\n} .
+         qq{\t) \r\n} .
+         qq{\tWHERE foo.a = 1 AND foo.b LIKE 'station' };
+
+      is($sqlat->format($sql), $expected_sql,
+         'subquery statement formatted correctly'
+      );
+   }
+
+   {
+      my $sql = "SELECT * FROM lolz WHERE ( foo.a =1 ) and foo.b LIKE 'station'";
+      my $expected_sql =
+         qq{SELECT * \r\n} .
+         qq{\tFROM lolz \r\n} .
+         qq{\tWHERE (foo.a = 1) AND foo.b LIKE 'station' };
+
+      is($sqlat->format($sql), $expected_sql,
+         'simple statement with parens in where formatted correctly'
+      );
+   }
+
+   {
+      my $sql = "SELECT [screen].[id], [screen].[name], [screen].[section_id], [screen].[xtype] FROM [users_roles] [me] JOIN [roles] [role] ON [role].[id] = [me].[role_id] JOIN [roles_permissions] [role_permissions] ON [role_permissions].[role_id] = [role].[id] JOIN [permissions] [permission] ON [permission].[id] = [role_permissions].[permission_id] JOIN [permissionscreens] [permission_screens] ON [permission_screens].[permission_id] = [permission].[id] JOIN [screens] [screen] ON [screen].[id] = [permission_screens].[screen_id] WHERE ( [me].[user_id] = ? ) GROUP BY [screen].[id], [screen].[name], [screen].[section_id], [screen].[xtype]";
+      my $expected_sql =
+         "SELECT [screen].[id], [screen].[name], [screen].[section_id], [screen].[xtype] \r\n".
+         "\tFROM [users_roles] [me] \r\n" .
+         "\tJOIN [roles] [role] \r\n" .
+         "\t\tON [role].[id] = [me].[role_id] \r\n" .
+         "\tJOIN [roles_permissions] [role_permissions] \r\n" .
+         "\t\tON [role_permissions].[role_id] = [role].[id] \r\n" .
+         "\tJOIN [permissions] [permission] \r\n" .
+         "\t\tON [permission].[id] = [role_permissions].[permission_id] \r\n" .
+         "\tJOIN [permissionscreens] [permission_screens] \r\n" .
+         "\t\tON [permission_screens].[permission_id] = [permission].[id] \r\n" .
+         "\tJOIN [screens] [screen] \r\n" .
+         "\t\tON [screen].[id] = [permission_screens].[screen_id] \r\n" .
+         "\tWHERE ([me].[user_id] = ?) \r\n" .
+         "\tGROUP BY [screen].[id], [screen].[name], [screen].[section_id], [screen].[xtype] ";
+
+      my $gotten = $sqlat->format($sql);
+      is($gotten, $expected_sql, 'real life statement 1 formatted correctly');
+   }
+   done_testing;
+};
+
+done_testing;
+# stuff we want:
+#    Max Width
+#    placeholder substitution




More information about the Bast-commits mailing list