【问题标题】:Guide on how to process AST formatted as JSON like structure有关如何处理格式为 JSON 结构的 AST 的指南
【发布时间】:2013-10-09 10:37:54
【问题描述】:

在 Perl 中使用 use SQL::Abstract::Tree,我可以通过以下方式为 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 函数,树在给定其类型和(这是每个子树中的第一个参数)的每个子树上调度,然后与树的其余部分一起调用它。这是我的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}}]};
}

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

另外,每个调度调用应该返回什么类型的数据,最后我该如何组合?

另外,我是 AST 处理的新手,并希望掌握它,因此我将不胜感激有关如何改进我的问题的建议。

【问题讨论】:

    标签: sql perl mongodb abstract-syntax-tree


    【解决方案1】:

    您进行类型调度的想法大致正确。通常人们可能会在它们上使用对象和调度方法。但是使用二元素列表来标记具有某种类型的数据也可以。您命名错误的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 case 更好,因为它们允许更多的验证。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2015-06-22
      • 2020-03-24
      • 2013-02-08
      • 1970-01-01
      • 1970-01-01
      • 2012-04-22
      • 2020-05-15
      相关资源
      最近更新 更多