4
3

More than 1 year has passed since last update.

TiDBのSQLパーサーを使ってみました

Last updated at Posted at 2022-12-11

Go言語で開発をしていると少なからずデータの出し入れ作業があるので自動化したいなと思うことが時々あります。

OASやORMあたりのライブラリを利用すると入出力まわりはたいていあるのですが、自分で作ってみたいなと思いMySqlのCreate文から構造を取得した際にTiDBのパーサーをつかってみたので簡単に紹介させていただきます。

TiDBはGo言語で実装され、mysqlとの高い互換性がありパーサーの利用方法もシンプルでした。
TiDBのパーサーの利用方法はquickstartを実施してもらうとすぐに利用できるようになりますが、TableのCreate文から必要な要素を抽出した処理を簡単にですが紹介させていただきます。

TiDB紹介

TiDB ("Ti" stands for Titanium) is an open-source NewSQL database that supports Hybrid Transactional and Analytical Processing (HTAP) workloads. It is MySQL compatible and features horizontal scalability, strong consistency, and high availability.

SQLパース

quickstart.md ではselect文のパースを行なっていますが、下記のcreate文のテーブル定義をGoの構造体に入れる例となります。
quickstart.md のParse SQL textのコード (https://github.com/pingcap/tidb/blob/master/parser/docs/quickstart.md#parse-sql-text) に追加していくことで動くようになっています。
Parse SQL text をローカル環境で動くようにしておくと動作確認できるようになっています。

var crateTableStmt = `
CREATE TABLE users (
	id BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID',
	name VARCHAR(10) NULL COMMENT '名前',
	birthday DATE NULL COMMENT '誕生日',
	PRIMARY KEY(id)
)COMMENT 'ユーザ情報';
`

SQLをパースする実装はQuickStartのサンプルを引用させていただきます。関数にSQLを渡すとパース結果を取得することができます。
複数のSQLをまとめてパースすることができ配列で返却されます。今回は検証で1個のSQLのため配列の先頭を返します。

func parse(sql string) (*ast.StmtNode, error) {
	p := parser.New()
	// 第2引数:charset 第3引数:collation
	stmtNodes, _, err := p.Parse(sql, "", "") 
	if err != nil {
		return nil, err
	}
	return &stmtNodes[0], nil 
}

StmtNodeとしてcreate文の要素が保持されています。そこから必要な定義を取得するには Visitorインターフェースを実装します。(https://pkg.go.dev/github.com/pingcap/tidb/parser/ast#Visitor)

type Visitor interface {
	Enter(n Node) (node Node, skipChildren bool)
	Leave(n Node) (node Node, ok bool)
}

今回はCreate Tableの要素を取得するインターフェース実装となります。以下の構造体に対してEnter/Leaveを実装します。

// CraateTable DDLからテーブル名とカラムの情報を保持する構造体
type CreateTable struct {
	TableName string     // テーブル名
	Columns   []Column   // カラム
	CurrentColumn Column // 分析中のカラムの情報を保持する
}
// カラムの情報を保持する構造体
type Column struct {
	Name     string // カラム名
	Type     string // 型
	Nullable bool   // Null可
	Comment  string
}

CreateTableの構造体を渡すことでパースされた定義をセットしていくようなメソッドを実装します。
データの構成に沿ってEnter/Leaveメソッドが呼ばれます。今回のSQLだと以下のようなネストした順番となります。(実際は記載していない複数のノードの呼び出しがあります)

1. Enter(テーブル:users)
2. Enter(カラム:id) ... Enter(コメント:ID) Leave(コメント:ID) Leave(カラム:id)
3. Enter(カラム:name) ... Leave(カラム:name)
4. Enter(カラム:birthday) ... Leave(カラム:birthday)
5. Leave(テーブル:users)

// Enterメソッド. 開始時に呼ばれる
func (v *CreateTable) Enter(node ast.Node) (ast.Node, bool) {
	// tableの抽出。型アサーションでマッチするNodeを判定します
	if tab, ok := node.(*ast.CreateTableStmt); ok {
		// .O はOriginal、.L はLower caseの文字列を返します
		v.TableName = tab.Table.Name.O 
	}
	// columnの抽出
	if col, ok := node.(*ast.ColumnDef); ok {
		v.CurrentColumn = Column{}
		// users.birthdayの場合 col.Name.Name.O=>birthday, col.Name.OrigColName()=>users.birthday
		v.CurrentColumn.Name = col.Name.Name.O
		// カラムの型
 		v.CurrentColumn.Type = col.Tp.String() 
	}
	// null/not nullの抽出
	if opt, ok := node.(*ast.ColumnOption); ok {
		if opt.Tp == ast.ColumnOptionNull {
			v.CurrentColumn.Nullable = true
		} else if opt.Tp == ast.ColumnOptionNotNull {
			v.CurrentColumn.Nullable = false
		}
	}
	// commentの抽出. ValueExprにキャストして値を取得します
	if opt, ok := node.(*ast.ColumnOption); ok && opt.Tp == ast.ColumnOptionComment {
		if val, ok := opt.Expr.(ast.ValueExpr); ok {
			v.CurrentColumn.Comment = val.GetDatumString()
		}
	}
	// bool は skipChildren でtrueを渡すことで子要素の処理をスキップすることができます。
	return node, false 
}

// Leaveメソッド. 終了時に呼ばれる
func (v *CreateTable) Leave(node ast.Node) (ast.Node, bool) {
	if _, ok := node.(*ast.ColumnDef); ok {
		// カラム定義が終了したら抽出した CurrentColumn を配列に追加
		v.Columns = append(v.Columns, v.CurrentColumn)
	}
	return node, true
}

では実行です。

func main(){
	stmtNode, err := parse(crateTableStmt) // SQLをパース
	if err != nil {
		panic(err)
	}
	createTable := CreateTable{} // Visitorインターフェースを実装している構造体
	createTable.Columns = []Column{}
	// Accept で Enter/Leaveが処理され、構造体に定義情報が設定されます。
	(*stmtNode).Accept(&createTable)
	fmt.Printf("%v\n", createTable) // 設定を出力
}

TiDBのパーサーの利用することでシンプルな実装でテーブル名、カラム名、型、null可、コメントを構造体に抽出することができました。

SQLを複数パースさせることもできます。

var crateTableStmt = `
CREATE TABLE users (
	id BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID'
)COMMENT 'ユーザ情報';

CREATE TABLE items (
	id BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID'
)COMMENT 'アイテム情報';
`

quickstart.md のParse SQL textでは最初のSQLを返すようにしているのでここを配列で返すようにすると複数のSQLを処理することもできます。

func parse(sql string) (*[]ast.StmtNode, error) {
	... // 省略
	return &stmtNodes, nil
}

さいごに

SQLパーサーのライブラリは多くありますが、mysql互換で使ったことのないものを選んでみました。ライブラリを読むことで実装方法など学べプロダクトにも興味をもつことができました。

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3