Zenn
Open8

tree-sitterで遊ぶ

ktz_aliasktz_alias

具象構文木生成ライブラリであるtree-sitterのさわりを学ぶ

本家
https://tree-sitter.github.io/tree-sitter/

2025-01-21時点で、tree-sitter-sqlの中でも一番スター数が多いDerekStride/tree-sitter-sqlを使う。
ちな、supabase-community/postgres_lspでも採用されてる。

遊ぶ言語はRust

ktz_aliasktz_alias

何か作る

rustのプロジェクト作成

cargo new app

依存の追加

cargo add tree-sitter
cargo add tree-sitter-sequel

ree-sitterクレートはパーザ本体。
tree-sitter-sequelDerekStride/tree-sitter-sqlから作られたパーザのソースに対してFFI処理を加えて使いやすくしてくれたもの。

main.rs

fn main() {
    let mut parser = tree_sitter::Parser::new();
    let lang = tree_sitter_sequel::LANGUAGE;

    parser.set_language(&lang.into()).expect("unsupported");

    let queries = vec![
        "select id, name, tel as t, 1 as x, ?::int as y from city"
    ];

    use std::os::fd::AsRawFd;
    let stdout_fd = std::io::stdout().as_raw_fd();

    for q in &queries {
        let ast = parser.parse(q, None).unwrap();
        // println!("{:?}", ast);
    }
}
{Tree {Node program (0, 0) - (0, 51)}}

ルートノードしか表示されなかった残念!

ktz_aliasktz_alias

改変

https://future-architect.github.io/articles/20221215a/ をコピってくる

    for q in &queries {
        let ast = parser.parse(q, None).unwrap();
-        println!("{:?}", ast);
+        visit(&mut ast.walk(), 0, q);
    }

const UNIT: usize = 2;

fn visit(cursor: &mut tree_sitter::TreeCursor, depth: usize, src: &str) {
    // インデント
    (0..(depth * UNIT)).for_each(|_| print!(" "));

    print!("{}", cursor.node().kind());

    // 子供がいないかつ、キーワードでない場合、対応する文字列を表示
    if cursor.node().child_count() == 0 && cursor.node().kind().chars().any(|c| c.is_lowercase()) {
        print!(" \"{}\"", cursor.node().utf8_text(src.as_bytes()).unwrap());
    }
    println!(
        " [{}-{}]",
        cursor.node().start_position(),
        cursor.node().end_position()
    );

    // 子供を走査
    if cursor.goto_first_child() {
        visit(cursor, depth + 1, src);
        while cursor.goto_next_sibling() {
            visit(cursor, depth + 1, src);
        }
        cursor.goto_parent();
    }
}
program [(0, 0)-(0, 51)]
  statement [(0, 0)-(0, 51)]
    select [(0, 0)-(0, 41)]
      keyword_select "select" [(0, 0)-(0, 6)]
      select_expression [(0, 7)-(0, 41)]
        term [(0, 7)-(0, 9)]
          field [(0, 7)-(0, 9)]
            identifier "id" [(0, 7)-(0, 9)]
        , [(0, 9)-(0, 10)]
        term [(0, 11)-(0, 15)]
          field [(0, 11)-(0, 15)]
            identifier "name" [(0, 11)-(0, 15)]
        , [(0, 15)-(0, 16)]
        term [(0, 17)-(0, 25)]
          field [(0, 17)-(0, 20)]
            identifier "tel" [(0, 17)-(0, 20)]
          keyword_as "as" [(0, 21)-(0, 23)]
          identifier "t" [(0, 24)-(0, 25)]
        , [(0, 25)-(0, 26)]
        term [(0, 27)-(0, 33)]
          literal "1" [(0, 27)-(0, 28)]
          keyword_as "as" [(0, 29)-(0, 31)]
          identifier "x" [(0, 32)-(0, 33)]
        , [(0, 33)-(0, 34)]
        term [(0, 35)-(0, 46)]
          cast [(0, 35)-(0, 41)]
            parameter "?" [(0, 35)-(0, 36)]
            :: [(0, 36)-(0, 38)]
            int [(0, 38)-(0, 41)]
              keyword_int "int" [(0, 38)-(0, 41)]
          keyword_as "as" [(0, 42)-(0, 44)]
          identifier "y" [(0, 45)-(0, 46)]
    from [(0, 42)-(0, 51)]
      keyword_from "from" [(0, 42)-(0, 46)]
      relation [(0, 47)-(0, 51)]
        object_reference [(0, 47)-(0, 51)]
          identifier "city" [(0, 47)-(0, 51)]

いい感じ!

ktz_aliasktz_alias

ダンプ結果からわかること

SELECT文の

  • selectkeyword_select
  • fromkeyword_from
  • select_expressionの子ノードにSELECTリストが並ぶ
    • リスト要素のルートはterm
      • 列の場合はfieldが子要素に
      • リテラルの場合はliteralが子要素に
        • 加えてエイリアスをつけていればidentifier要素が生える
      • パラメータの場合はparameterが子要素に
      • 型付きパラメータの場合はcast要素が生え、その子要素にparameterが来る
  • keyword_fromの子要素にrelationが来る場合、子孫にテーブル名が鎮座する
ktz_aliasktz_alias

別のSQL

SELECT t.id from generate_series(1, 10) t(id)を試してみる。

program [(0, 0)-(0, 45)]
  statement [(0, 0)-(0, 45)]
    select [(0, 0)-(0, 11)]
      keyword_select "SELECT" [(0, 0)-(0, 6)]
      select_expression [(0, 7)-(0, 11)]
        term [(0, 7)-(0, 11)]
          field [(0, 7)-(0, 11)]
            object_reference [(0, 7)-(0, 8)]
              identifier "t" [(0, 7)-(0, 8)]
            . [(0, 8)-(0, 9)]
            identifier "id" [(0, 9)-(0, 11)]
    from [(0, 12)-(0, 45)]
      keyword_from "from" [(0, 12)-(0, 16)]
      relation [(0, 17)-(0, 45)]
        invocation [(0, 17)-(0, 39)]
          object_reference [(0, 17)-(0, 32)]
            identifier "generate_series" [(0, 17)-(0, 32)]
          ( [(0, 32)-(0, 33)]
          term [(0, 33)-(0, 34)]
            literal "1" [(0, 33)-(0, 34)]
          , [(0, 34)-(0, 35)]
          term [(0, 36)-(0, 38)]
            literal "10" [(0, 36)-(0, 38)]
          ) [(0, 38)-(0, 39)]
        identifier "t" [(0, 40)-(0, 41)]
        list [(0, 41)-(0, 45)]
          ( [(0, 41)-(0, 42)]
          column [(0, 42)-(0, 44)]
            identifier "id" [(0, 42)-(0, 44)]
          ) [(0, 44)-(0, 45)]
  • 表関数の場合もkeyword_fromの直下にrelationが来ることは変わらない。
  • object_referenceの代わりにinvocationでラップされる。
  • ()の間に引数リスト
    • 引数リストはSELECTリストと同じ感じ
  • 表関数に別名があればidentifierが続く
    • 列名まで指定した時はさらにlistが続く
      • listの子要素はcolumn
ktz_aliasktz_alias

表関数名を間違ってみる

SELECT t.id from generate_serie(1, 10) t(id)をパースする。

  • generate_seriesの最後のs忘れ
    from [(0, 12)-(0, 44)]
      keyword_from "from" [(0, 12)-(0, 16)]
      relation [(0, 17)-(0, 44)]
        invocation [(0, 17)-(0, 38)]
          object_reference [(0, 17)-(0, 31)]
            identifier "generate_serie" [(0, 17)-(0, 31)]
          ( [(0, 31)-(0, 32)]
          term [(0, 32)-(0, 33)]
            literal "1" [(0, 32)-(0, 33)]
          , [(0, 33)-(0, 34)]
          term [(0, 35)-(0, 37)]
            literal "10" [(0, 35)-(0, 37)]
          ) [(0, 37)-(0, 38)]
        identifier "t" [(0, 39)-(0, 40)]
        list [(0, 40)-(0, 44)]
          ( [(0, 40)-(0, 41)]
          column [(0, 41)-(0, 43)]
            identifier "id" [(0, 41)-(0, 43)]
          ) [(0, 43)-(0, 44)]

構文木を作ることが目的なのでそこまでチェックはしない(あたりまえ)

ログインするとコメントできます