Open8
tree-sitterで遊ぶ

具象構文木生成ライブラリであるtree-sitter
のさわりを学ぶ
本家
2025-01-21時点で、tree-sitter-sql
の中でも一番スター数が多いDerekStride/tree-sitter-sql
を使う。
ちな、supabase-community/postgres_lsp
でも採用されてる。
遊ぶ言語はRust
で

サンプルを探す
程よいサンプルはないかと彷徨っていて、Json
パーザのちっさいサンプルを見つけた。
加えてツリーをダンプするものはないかと彷徨ったところ、いい感じのがあった。

何か作る
rustのプロジェクト作成
cargo new app
依存の追加
cargo add tree-sitter
cargo add tree-sitter-sequel
ree-sitter
クレートはパーザ本体。
tree-sitter-sequel
はDerekStride/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)}}
ルートノードしか表示されなかった残念!

改変
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)]
いい感じ!

ダンプ結果からわかること
SELECT
文の
-
select
はkeyword_select
-
from
はkeyword_from
-
select_expression
の子ノードにSELECTリストが並ぶ- リスト要素のルートは
term
- 列の場合は
field
が子要素に - リテラルの場合は
literal
が子要素に- 加えてエイリアスをつけていれば
identifier
要素が生える
- 加えてエイリアスをつけていれば
- パラメータの場合は
parameter
が子要素に - 型付きパラメータの場合は
cast
要素が生え、その子要素にparameter
が来る
- 列の場合は
- リスト要素のルートは
-
keyword_from
の子要素にrelation
が来る場合、子孫にテーブル名が鎮座する

別の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
-
- 列名まで指定した時はさらに

表関数名を間違ってみる
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)]
構文木を作ることが目的なのでそこまでチェックはしない(あたりまえ)
ログインするとコメントできます