GolangでMySQLのSQL文をパースする(CREATE TABLE文のパース例)
TL;DR
Go言語で https://github.com/pingcap/tidb/ パッケージを使用してMySQLのSQL文をパースできる。
CREATE TABLE文をパースするサンプルコード。
背景
MySQLのテーブル構造の情報をGo言語で扱いたかった。
テーブル構造をCREATE TABLE文で管理している前提のため
SQL(CREATE TABLE)文をGo言語でパースできれば良い。
対象のCREATE TABLE文
今回はこのCREATE TABLE文をパース対象としています。
CREATE TABLE IF NOT EXISTS users (
id SERIAL,
name VARCHAR(40) NOT NULL,
age SMALLINT UNSIGNED DEFAULT 0,
address_id BIGINT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX name_index (name),
FOREIGN KEY (address_id) REFERENCES addresses (id)
);
使用ライブラリ
使用したライブラリはこちら。
クイックスタートのドキュメントはこちら
セットアップ
mkdir golang-parse-sql
cd golang-parse-sql
go mod init example.com/m
echo 'package main\n\nimport (\n\t"github.com/pingcap/tidb/parser"\n\t"github.com/pingcap/tidb/parser/ast"\n\t_ "github.com/pingcap/tidb/parser/test_driver"\n)\n\nfunc main() {}' > main.go
go mod tidy
取得したい情報の詳細
今回はテーブルの名前、全ての列名、列の型が最低限欲しい情報です。
インデックスや外部キー制約の情報も取得できますが、今回のコードでは省略しました。
CREATE TABLE文から欲しい情報を取得する。
SQL文を入力として、テーブルの名前、全ての列名、列の型を取得するコードがこちらです。
コードの大半が定型文で、Your Handler
というコメントをした
func (v *visitor) Enter(in ast.Node) (ast.Node, bool)
関数が主な処理です。
package main
import (
"fmt"
"log/slog"
"github.com/pingcap/tidb/parser"
"github.com/pingcap/tidb/parser/ast"
_ "github.com/pingcap/tidb/parser/test_driver"
)
const sql = `
CREATE TABLE IF NOT EXISTS users (
id SERIAL,
name VARCHAR(40) NOT NULL,
age SMALLINT UNSIGNED DEFAULT 0,
address_id BIGINT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX name_index (name),
FOREIGN KEY (address_id) REFERENCES addresses (id)
);
SELECT 1;
`
type (
visitor struct {
IsCreateTable bool
TableName string
Cols []columnInfo
}
columnInfo struct {
Name string
Type string
CompactType string
}
)
// Your Handler
func (v *visitor) Enter(in ast.Node) (ast.Node, bool) {
// slog.Debug("Enter node", "type", reflect.TypeOf(in), "val", fmt.Sprintf("%#v",in))
if createTable, ok := in.(*ast.CreateTableStmt); ok {
v.IsCreateTable = true
v.TableName = createTable.Table.Name.O
}
if def, ok := in.(*ast.ColumnDef); ok {
c := columnInfo{
Name: def.Name.Name.O,
Type: def.Tp.String(),
CompactType: def.Tp.CompactStr(),
}
v.Cols = append(v.Cols, c)
return in, false
}
return in, false
}
// 多くの場合で定型文
func (v *visitor) Leave(in ast.Node) (ast.Node, bool) {
return in, true
}
// 定型文
func extract(rootNode *ast.StmtNode) *visitor {
v := &visitor{}
(*rootNode).Accept(v)
return v
}
// 定型文
func parse(sql string) ([]ast.StmtNode, error) {
stmtNodes, warns, err := parser.New().Parse(sql, "", "")
if warns != nil {
slog.Warn("Parse warnings", "warns", warns)
}
if err != nil {
return nil, err
}
return stmtNodes, nil
}
func main() {
astNodes, err := parse(sql)
if err != nil {
fmt.Printf("parse error: %v\n", err.Error())
return
}
// Create Table文, Select文の2要素
for _, astNode := range astNodes {
fmt.Println(extract(&astNode))
}
}
ライブラリとコードの解説をざっくり
ライブラリが、クエリのパースとパース結果の走査を行ってくれます。
走査時の処理をこちらが実装することで欲しい情報を集めます。
具体的にはこのインターフェースを実装します。
type Visitor interface {
Enter(n Node) (node Node, skipChildren bool)
Leave(n Node) (node Node, ok bool)
}
パース結果の木構造を走査するときの、ノードにたどり着いた時の処理を Enter
関数として
ノードを遡る時の処理を Leave
関数として実装します。
Enter
関数の二つ目の返り値をtrueにすることで、そのノードの子要素の走査をスキップできます。
今回のクエリに対するパース結果の詳細
下記コードでEnter関数に渡される要素の型を表示することで、このような木構造としてパースされていることがわかりました。
func (v visitor) Enter(n Node) (node Node, skipChildren bool) {
fmt.Println(refrect.TypeOf(n))
}
*ast.CreateTableStmt
*ast.TableName
*ast.ColumnDef
*ast.ColumnName
*ast.ColumnOption
*ast.ColumnOption
*ast.ColumnOption
*ast.ColumnDef
*ast.ColumnName
*ast.ColumnOption // NOT NULL
*ast.ColumnDef
*ast.ColumnName
*ast.ColumnOption // DEFAULT
*test_driver.ValueExpr // DEFAULT val
*ast.ColumnDef
*ast.ColumnName
*ast.ColumnDef
*ast.ColumnName
*ast.ColumnOption
*ast.FuncCallExpr
*ast.ColumnDef
*ast.ColumnName
*ast.ColumnOption
*ast.FuncCallExpr
*ast.ColumnOption // ON UPDATE
*ast.FuncCallExpr
*ast.Constraint // インデックス
*ast.IndexPartSpecification
*ast.ColumnName
*ast.Constraint // 外部キー制約
*ast.IndexPartSpecification
*ast.ColumnName
*ast.ReferenceDef
*ast.TableName
*ast.IndexPartSpecification
*ast.ColumnName
*ast.OnDeleteOpt
*ast.OnUpdateOpt
NOT NULL
, DEFAULT
, ON UPDATE
などの情報もちゃんと取得できるようでした。
Discussion