📘

GolangでMySQLのSQL文をパースする(CREATE TABLE文のパース例)

2023/12/06に公開

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)
);

使用ライブラリ

使用したライブラリはこちら。
https://github.com/pingcap/tidb/

クイックスタートのドキュメントはこちら

https://github.com/pingcap/tidb/blob/master/pkg/parser/docs/quickstart.md

セットアップ

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