代码之家  ›  专栏  ›  技术社区  ›  DJG

关于如何处理格式化为类似JSON结构的AST的指南

  •  0
  • DJG  · 技术社区  · 11 年前

    具有 use SQL::Abstract::Tree 在Perl中,我能够通过以下方式为SQL生成AST:

    my $sqlat = SQL::Abstract::Tree->new;
    my $tree = $sqlat->parse($query_str);
    

    哪里 $query_str 是一个SQL查询。

    例如,使用查询字符串 SELECT cust_id, a as A, z SUM(price) as q, from orders WHERE status > 55 ,产生:

    [
      [
        "SELECT",
        [
          [
            "-LIST",
            [
              ["-LITERAL", ["cust_id"]],
              ["AS", [["-LITERAL", ["a"]], ["-LITERAL", ["A"]]]],
              [
                "AS",
                [
                  ["-LITERAL", ["z"]],
                  ["SUM", [["-PAREN", [["-LITERAL", ["price"]]]]]],
                  ["-LITERAL", ["q"]],
                ],
              ],
              [],
            ],
          ],
        ],
      ],
      ["FROM", [["-LITERAL", ["orders"]]]],
      [
        "WHERE",
        [[">", [["-LITERAL", ["status"]], ["-LITERAL", [55]]]]],
      ],
    ]
    

    我想浏览AST并获得有关它的某些信息。

    我想知道是否有一个指南/教程/示例源代码可以以这种格式引导AST。

    我发现的大多数考虑行走AST的文献通常假设我有某种类层次结构,描述了行走AST的访问者模式的某种变化。

    我的具体用例是将简单的SQL查询转换为聚合框架的Mongo查询,并给出了一些示例 here .

    以下是我到目前为止一直在做的事情:

    我先打电话给 parse 具有树的函数在给定其类型和(这是每个子树中的第一个参数)的情况下对每个子树进行调度,并用树的其余部分调用它。这是我的 作语法分析 功能:

    sub parse {
        my ($tree) = @_;
    
        my %results = (ret => []);
        for my $subtree (@$tree) {
            my ($node_type, $node) = @$subtree;
    
            my $result_dic = $dispatch{$node_type}->($node);
            if ($result_dic->{type}) {
                 my $type = $result_dic->{type};
                 $results{$type} = [] unless $results{$type};
                 push $results{$type}, $result_dic->{ret};
                 %results = merge_except_for($result_dic, \%results, 'ret', $type);
             }
             else {
                 push @{$results{ret}}, @{$result_dic->{ret}};
             }
    
        }
    
    
        return \%results;
    
    }
    

    使用以下调度表:

    my %dispatch = (
        SELECT => sub {
    
            my $node = shift;
            my $result_dic = parse($node);
            $result_dic->{type} = 'select';
            if ($result_dic->{as}) {
                 push $result_dic->{ret}, $result_dic->{as}->[0][0];
             }
            return $result_dic;
        },
        '-LITERAL' => sub {
            my $node = shift;
            my $literal = $node;
            return {ret => $node};
        },
        '-LIST' => sub {
            my $node = shift;
            my $result_dic = parse($node);
    
            my $ret = flatten_ret($result_dic);
    
            return flatten_ret($result_dic);
        },
        WHERE => sub {
            my $tree = shift;
            my @bin_ops = qw/= <= < >= >/;
    
            my $op = $tree->[0];
            if ($op ~~ @bin_ops) {
                # Not yet implemented
            }
            return {ret => ''};
    
        },
        FROM => sub {
            my $tree = shift;
            my $parse_result = parse($tree);
            return {ret => $parse_result->{ret},
                    type => 'database'};
        },
        AS => sub {
            my $node = shift;
    
            my $result_dic = parse($node);
            $result_dic->{type} = 'as';
            return $result_dic;
        }
    );
    
    sub flatten_ret {
        my $result_dic = shift;
    
        return {ret => [
            map {
                ref($_) ? $_->[0] : $_
            } @{$result_dic->{ret}}]};
    }
    

    但我不确定某些事情,比如我是否应该检查节点名是否为 "AS" SELECT 子程序或找到一种递归的方法来填充数据。

    此外,每次调度调用应该返回什么类型的数据,以及如何在最后将其组合?

    此外,我是AST处理的新手,希望能掌握它,所以关于如何改进我的问题的建议也将不胜感激。

    1 回复  |  直到 11 年前
        1
  •  1
  •   amon    11 年前

    你做打字调度的想法大致正确。通常情况下,可能会在对象上使用对象和分派方法。但是,使用两个元素的列表来标记具有某种类型的数据也是可行的。你用词不当 parse 函数实现了这个调度,并以某种方式聚合了输出。我不太确定你想用它实现什么。

    在进行AST转换时,记住要创建的确切输出是非常有用的。假设您想要转换

    SELECT cust_id, a as A, SUM(price) as q from orders WHERE status > 55
    

    进入数据结构

    {
      table  => 'orders',
      action => 'aggregate',
      query  => [
        '$match' => { 'status' => { '$gt' => 55 } },
        '$group' => {
           '_id'     => undef,
           'cust_id' => '$cust_id',
           'A'       => '$a',
           'q'       => { '$sum' => '$price' },
        },
      ],
    }
    

    我们该怎么办?

    • 断言我们有 SELECT ... FROM ... 类型查询。
    • 将操作设置为 aggregate .
    • 提取的表名 FROM 进入
    • 组装查询:
      • 对于每个 SELECT 项,获取名称以及生成该值的表达式。
        • 递归生成每个表达式
      • 如果 WHERE 子句存在,递归地翻译每个条件。

    如果我们遇到无法解析的语法,那么抛出一个错误。

    请注意,我的方法从顶部开始,并在需要时从AST的更深层次提取信息。这与自下而上的方法形成了鲜明对比,这种方法将所有数据拼凑在一起,并希望最终保留一些相关的信息。尤其是你的哈希合并看起来很可疑。

    如何实现这一点?这是一个开始:

    use Carp;
    
    sub translate_select_statement {
      my ($select, $from, @other_clauses) = @_;
      $select->[0] eq 'SELECT'
        or croak "First clause must be a SELECT clause, not $select->[0]";
      $from->[0] eq 'FROM'
        or croak "Second clause must be a FROM clause, not $from->[0]";
    
      my $select_list = $select->[1];
      my %groups = (
        _id => undef,
        translate_select_list(get_list_items($select_list)),
      );
    
      ...
    }
    
    sub get_list_items {
      my ($list) = @_;
      if ($list->[0] eq '-LIST') {
        return @{ $list->[1] };
      }
      else {
        # so it's probably just a single item
        return $list;
      }
    };
    
    sub translate_select_list {
      my %out;
      for my $item (@_) {
        my ($type, $data) = @$item;
        if ($type eq '-LITERAL') {
          my ($name) = @$data;
          $out{$name} = '$' . $name;
        }
        elsif ($type eq '-AS') {
          my ($expr, $name_literal) = @$data;
          $name_literal->[0] eq '-LITERAL'
            or croak "in 'x AS y' expression, y must be a literal, but it was $name_literal->[0]";
          $out{$name_literal->[1][0]} = translate_expression($expr);
        }
        else {
          croak "I select list, items must be literals or 'x AS y' expression. Found [$type, $data] instead.";
        }
      }
      return %out;
    }
    
    sub translate_expression { ... }
    

    我构建它的方式更像是一个自上而下的解析器,但例如,对于算术表达式的翻译,类型调度更重要。在上述代码中, if / else 案例更好,因为它们允许更多的验证。