sqlc generate は何をしているのかメモ
[2023-09-11 追記] 以下のブログの方が分かりやすいので、そちらを読むことをお勧めします。
最近、個人開発で Go のアプリケーションからデータベースを操作するために sqlc を使っている。sqlc は SQL で書いたスキーマのファイルから sqlc generate
コマンド一つで Go の構造体を、クエリのファイルから型安全な Go の SQL クライアントを生成することができる。1
私は普段の仕事で ORM が提供するメソッドやクエリビルダを使って SQL を組み立てている。ORM は便利ではあるが、最終的に実行される SQL は可読性が低くてデバッグしづらいことが多い。そのため「最初から SQL を書けば良いのでは…?」と考えるようになり、初めて sqlc を見たとき自分の好みに合うツールと確信した。
使い方は簡単で便利なのだが、どうやって sqlc generate
コマンドで Go のコードを生成しているのか想像がつかなかったのでコードを読んでみることにした。対象バージョンは v1.20.0 とする。
sqlc の使い方
まずは sqlc の使い方をおさらいする。
- スキーマとクエリを SQL で書く
sqlc generate
コマンドで Go のメソッドと構造体を生成する- 生成したメソッドと構造体を自分のアプリケーションから呼び出す
以下のようなディレクトリ構成で簡単なアプリケーションを作る。
.
├── database
│ ├── queries
│ │ └── queries.sql
│ └── schema
│ └── schema.sql
├── generated
│ ├── db.go
│ ├── models.go
│ └── queries.sql.go
├── main.go
└── sqlc.yaml
以下では例として PostgreSQL を使用するが、sqlc は MySQL でも SQLite でも使える。
まずは SQL でスキーマを書く。
CREATE TABLE IF NOT EXISTS "authors" (
"id" BIGSERIAL PRIMARY KEY,
"name" text NOT NULL,
"bio" text
);
次に SQL でデータベースを操作するクエリを書く。このとき、メソッド名と適用するコマンドをコメントで書いておく。使えるコマンドは公式ドキュメントに記載されている。
https://docs.sqlc.dev/en/stable/reference/query-annotations.html
:one
や :many
等のコマンドによってクエリを実行するメソッドの返り値の型を定義することができる。:batchMany
などで PostgreSQL のドライバー pgx を使ってバッチ処理をすることもできる。業務システムで大量のデータをバッチ処理する際には有用そうだ。
-- name: GetAuthor :one
SELECT * FROM authors
WHERE id = $1 LIMIT 1;
-- name: ListAuthors :many
SELECT * FROM authors
ORDER BY name;
-- name: CreateAuthor :one
INSERT INTO authors (
name, bio
) VALUES (
$1, $2
)
RETURNING *;
-- name: UpdateAuthor :one
UPDATE authors
set name = $2,
bio = $3
WHERE id = $1
RETURNING *;
-- name: DeleteAuthor :exec
DELETE FROM authors
WHERE id = $1;
sqlc では SQL や生成する Go ファイルの配置先を設定するファイルを YAML や JSON で書く。
https://docs.sqlc.dev/en/stable/reference/config.html
例えば “./database/” 以下の SQL ファイルでできたスキーマとクエリを “tutorial” というパッケージ名で “./generated/” 以下に出力する設定は YAML で以下のように書くことができる。
version: "2"
sql:
- engine: "postgresql"
queries: "./database/queries/queries.sql"
schema: "/database/schema/schema.sql"
gen:
go:
package: "tutorial"
out: "./generated"
ここまでの準備ができたら sqlc コマンドを sqlc の設定ファイルがあるディレクトリで実行する。
$ sqlc generate
これによりデータベースのクライアントとなる Go のメソッドと構造体を生成できる。ここからは実際に何が生成されたのかを見ていく。
まず db.go
として、データベースへの接続やトランザクション管理などデータベース操作に関連したメソッドが自動生成されている。
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.20.0
package tutorial
import (
"context"
"database/sql"
)
type DBTX interface {
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
PrepareContext(context.Context, string) (*sql.Stmt, error)
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
return &Queries{
db: tx,
}
}
ここで DBTX
インターフェイスが定義する ExecContext などのメソッドは Go の database/sql パッケージ内の複数の型で実装されている。
次に、models.go
としてデータベースのスキーマに沿った構造体が生成されている。
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.20.0
package tutorial
import (
"database/sql"
)
type Author struct {
ID int64
Name string
Bio sql.NullString
}
これは sqlc で自動生成したデータの取得・操作メソッドが返す値の型として自分のアプリケーションで使うことができる。
ここで生成されるファイルは設定ファイルやプラグインでカスタマイズできる。例えば先述の config ファイルで emit_json_tags
を true にすると、以下のように構造体へ json タグを付与することができる。
type Author struct {
ID int64 `json:"id"`
Name string `json:"name"`
Bio sql.NullString `json:"bio"`
}
このタグはコード上ではただのコメントに過ぎないが、リフレクションを活用するパッケージを介して構造体のメタデータとして使うことができる。例えば encoding/json では JSON のフィールド名を構造体のフィールドにマッピングすることができる。これにより、外部から受け取った JSON をスムーズに Go の構造体へ読み込むことが簡単になる。
最後に queries.sql.go
を見てみる。ここにはデータを取得・操作するメソッドが生成されている。
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.20.0
// source: queries.sql
package tutorial
import (
"context"
"database/sql"
)
const createAuthor = `-- name: CreateAuthor :one
INSERT INTO authors (
name, bio
) VALUES (
$1, $2
)
RETURNING id, name, bio
`
type CreateAuthorParams struct {
Name string
Bio sql.NullString
}
func (q *Queries) CreateAuthor(ctx context.Context, arg CreateAuthorParams) (Author, error) {
row := q.db.QueryRowContext(ctx, createAuthor, arg.Name, arg.Bio)
var i Author
err := row.Scan(&i.ID, &i.Name, &i.Bio)
return i, err
}
const deleteAuthor = `-- name: DeleteAuthor :exec
DELETE FROM authors
WHERE id = $1
`
func (q *Queries) DeleteAuthor(ctx context.Context, id int64) error {
_, err := q.db.ExecContext(ctx, deleteAuthor, id)
return err
}
const getAuthor = `-- name: GetAuthor :one
SELECT id, name, bio FROM authors
WHERE id = $1 LIMIT 1
`
func (q *Queries) GetAuthor(ctx context.Context, id int64) (Author, error) {
row := q.db.QueryRowContext(ctx, getAuthor, id)
var i Author
err := row.Scan(&i.ID, &i.Name, &i.Bio)
return i, err
}
const listAuthors = `-- name: ListAuthors :many
SELECT id, name, bio FROM authors
ORDER BY name
`
func (q *Queries) ListAuthors(ctx context.Context) ([]Author, error) {
rows, err := q.db.QueryContext(ctx, listAuthors)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Author
for rows.Next() {
var i Author
if err := rows.Scan(&i.ID, &i.Name, &i.Bio); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateAuthor = `-- name: UpdateAuthor :one
UPDATE authors
set name = $2,
bio = $3
WHERE id = $1
RETURNING id, name, bio
`
type UpdateAuthorParams struct {
ID int64
Name string
Bio sql.NullString
}
func (q *Queries) UpdateAuthor(ctx context.Context, arg UpdateAuthorParams) (Author, error) {
row := q.db.QueryRowContext(ctx, updateAuthor, arg.ID, arg.Name, arg.Bio)
var i Author
err := row.Scan(&i.ID, &i.Name, &i.Bio)
return i, err
}
生成されたコードをよく見ると 2 ~ 3 個のブロックが 1 セットで生成されていることがわかる。
- SQL のクエリを格納する定数
- (パラメータが複数ある場合)パラメータを示す型
- クエリを実行するためのメソッド(Context と 2. を引数に取る)
最初に書いた SQL は const で定数として定義されている。例えば authors テーブルに 1 レコードを追加する SQL を見ると…
-- name: CreateAuthor :one
INSERT INTO authors (
name, bio
) VALUES (
$1, $2
)
RETURNING *;
SQL 部分は *
がカラム名になっている以外は同じ。
const createAuthor = `-- name: CreateAuthor :one
INSERT INTO authors (
name, bio
) VALUES (
$1, $2
)
RETURNING id, name, bio
`
type CreateAuthorParams struct {
Name string
Bio sql.NullString
}
func (q *Queries) CreateAuthor(ctx context.Context, arg CreateAuthorParams) (Author, error) {
row := q.db.QueryRowContext(ctx, createAuthor, arg.Name, arg.Bio)
var i Author
err := row.Scan(&i.ID, &i.Name, &i.Bio)
return i, err
}
実際にこれを使うには、生成したメソッドと構造体を自分のアプリケーションから呼び出す。
ctx := context.Background()
db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
if err != nil {
return err
}
defer db.Close()
client := tutorial.New(db)
newAuthor, err := client.CreateAuthor(ctx, tutorial.CreateAuthorParams{
Name: "new author",
Bio: sql.NullString{
String: "new author bio",
Valid: true,
},
})
if err != nil {
return err
}
上記のように、
- SQL を書く
- 1 から Go の構造体とメソッドを自動生成する
- 2 を自分のアプリケーションから呼び出す
という 3 step で簡単にデータベースを操作するプログラムを書くことができてとても便利なパッケージであることが分かる。
sqlc generate
は何をしているのか
ここから本題に入る。sqlc でコードを生成する generate コマンドが何をしているのか、愚直に最初から処理を辿っていく。
エントリーポイント
sqlc の main.go は次のようになっている。sqlc generate
を実行すると cmd.Do の 1 つ目の引数として ["generate"]
が渡される。
package main
import (
"os"
"github.com/sqlc-dev/sqlc/internal/cmd"
)
func main() {
os.Exit(cmd.Do(os.Args[1:], os.Stdin, os.Stdout, os.Stderr))
}
コマンドの実行に関する設定
internal/cmd の Do 関数 では、コマンドラインインターフェイスを作成するための外部パッケージとして Cobra を使っている。Go は標準パッケージ flag を使うことで CLI を作れるが、Cobra を使うとサブコマンド(例えば git add
や git commit
のような階層的なコマンド)を簡単に作れるというメリットがある。
Do 関数を見ていく(コメントは筆者が挿入した)。
func Do(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) int {
// ルートコマンドを初期化する
// "git commit" における "git"
rootCmd := &cobra.Command{Use: "sqlc", SilenceUsage: true}
// 全てのコマンドで使用されるコマンドラインオプションを追加する
rootCmd.PersistentFlags().StringP("file", "f", "", "specify an alternate config file (default: sqlc.yaml)")
// ...省略...
// サブコマンドを追加する
// "git commit" における "commit"
rootCmd.AddCommand(checkCmd)
// ...省略...
// ルートコマンドに対して引数, 入力, 出力, エラー出力を追加する
// args, stdin, stdout, stderr は Do 関数の引数として受け取ったものをそのまま渡している
rootCmd.SetArgs(args)
rootCmd.SetIn(stdin)
rootCmd.SetOut(stdout)
rootCmd.SetErr(stderr)
ctx := context.Background()
// デバッグ用のトレースを初期化する
if debug.Debug.Trace != "" {
tracectx, cleanup, err := tracer.Start(ctx)
if err != nil {
fmt.Printf("failed to start trace: %v\n", err)
return 1
}
ctx = tracectx
defer cleanup()
}
// コマンドを実行する
if err := rootCmd.ExecuteContext(ctx); err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
return exitError.ExitCode()
} else {
return 1
}
}
return 0
}
generate コマンドの実装
sqlc generate
すると上記の ExecuteContext(ctx)
の箇所で genCmd
が呼ばれるので、次は genCmd がどのように定義されているかを見てみる。
var genCmd = &cobra.Command{
Use: "generate",
Short: "Generate Go code from SQL",
RunE: func(cmd *cobra.Command, args []string) error {
defer trace.StartRegion(cmd.Context(), "generate").End()
stderr := cmd.ErrOrStderr()
dir, name := getConfigPath(stderr, cmd.Flag("file"))
output, err := Generate(cmd.Context(), ParseEnv(cmd), dir, name, stderr)
if err != nil {
os.Exit(1)
}
defer trace.StartRegion(cmd.Context(), "writefiles").End()
for filename, source := range output {
os.MkdirAll(filepath.Dir(filename), 0755)
if err := os.WriteFile(filename, []byte(source), 0644); err != nil {
fmt.Fprintf(stderr, "%s: %s\n", filename, err)
return err
}
}
return nil
},
}
8 行目の Generate 関数で出力を生成しているようなので、次は Generate 関数を見ていく。
Generate 関数の実装
Generate 関数は internal/cmd/generate.go で定義されている。長い関数なので、少しずつ分割しながら読む。
設定ファイルを読む
まずは readConfig で設定ファイルを読み込む。
configPath, conf, err := readConfig(stderr, dir, filename)
if err != nil {
return nil, err
}
base := filepath.Base(configPath)
if err := config.Validate(conf); err != nil {
fmt.Fprintf(stderr, "error validating %s: %s\n", base, err)
return nil, err
}
if err := e.Validate(conf); err != nil {
fmt.Fprintf(stderr, "error validating %s: %s\n", base, err)
return nil, err
}
readConfig では ParseConfig を呼ぶことで version ごとに別の関数を呼び出している。
v2 の場合は v2ParseConfig が呼ばれる。v2ParseConfig では io.Reader から入力として設定を読み込み、それを Config 型のインスタンスとして YAML をデコードして解析する。
リモート生成
コードのリモート生成が設定で有効にされている場合は remoteGenerate
を呼び出す。
if conf.Cloud.Project != "" && !e.NoRemote {
return remoteGenerate(ctx, configPath, conf, dir, stderr)
}
remoteGenerate の実装は以下のようになっている。
この関数はリモートのサービスにリクエストを送信して生成されたコードを受け取るためのクライアント側の処理で、日常的に使うことは少ないかもしれない。詳細は Discussions #2234 を参照。sqlc は将来的にこれで収益化するつもりなのかもしれない。
出力の準備
最終的に出力する output map[string]string{}
とそれを作るための pairs []outPair
を作る。pairs にはどのようなコード(例えば Go)を生成するか、どのプラグインを使って生成するか設定を追加していく。
output := map[string]string{}
errored := false
var pairs []outPair
for _, sql := range conf.SQL {
if sql.Gen.Go != nil {
pairs = append(pairs, outPair{
SQL: sql,
Gen: config.SQLGen{Go: sql.Gen.Go},
})
}
if sql.Gen.JSON != nil {
pairs = append(pairs, outPair{
SQL: sql,
Gen: config.SQLGen{JSON: sql.Gen.JSON},
})
}
for i, _ := range sql.Codegen {
pairs = append(pairs, outPair{
SQL: sql,
Plugin: &sql.Codegen[i],
})
}
}
例えば以下のような sqlc.yaml で generate する場合を考える。
version: "2"
sql:
- engine: "postgresql"
queries: "./database/queries/queries.sql"
schema: "/database/schema/schema.sql"
gen:
go:
package: "tutorial"
out: "./generated"
sql.Gen.Go != nil
なので pairs に下記の outPair が追加される。
outPair{
SQL: sql,
Gen: config.SQLGen{Go: sql.Gen.Go},
}
ここで追加した outPair は次の「並列処理の準備」のさらに次の「コードの生成」の箇所でイテレーションに使われる。
並列処理の準備
同時実行に関する Mutex を初期化する。出力の書き込み時に競合を避けるために使う。
var m sync.Mutex
grp, gctx := errgroup.WithContext(ctx)
grp.SetLimit(runtime.GOMAXPROCS(0))
stderrs := make([]bytes.Buffer, len(pairs))
コードの生成
pairs スライスに格納された出力ペアをイテレートし、それぞれのペアについて SQL の解析(parse
)とコード生成(codegen
)の処理を非同期的に並列実行していく。
// TODO:
以外のコメントは筆者が挿入した。
// ↑ で作った pairs をイテレートしていく
for i, pair := range pairs {
// 現在の pair を格納する
sql := pair
errout := &stderrs[i]
// 新しい goroutine を起動して非同期的に各ペアの処理を実行する
grp.Go(func() error {
// 全体の設定と現在の pair に入っている SQL オプションを結合する
combo := config.Combine(*conf, sql.SQL)
// プラグインの設定があれば追加する
if sql.Plugin != nil {
combo.Codegen = *sql.Plugin
}
// TODO: This feels like a hack that will bite us later
joined := make([]string, 0, len(sql.Schema))
// スキーマが書かれた SQL ファイルのパスを設定に追加する
for _, s := range sql.Schema {
joined = append(joined, filepath.Join(dir, s))
}
sql.Schema = joined
joined = make([]string, 0, len(sql.Queries))
// クエリが書かれた SQL ファイルのパスを設定に追加する
for _, q := range sql.Queries {
joined = append(joined, filepath.Join(dir, q))
}
sql.Queries = joined
var name, lang string
parseOpts := opts.Parser{
Debug: debug.Debug,
}
// Go かプラグインかに応じて、名前を設定する
switch {
case sql.Gen.Go != nil:
name = combo.Go.Package
lang = "golang"
case sql.Plugin != nil:
lang = fmt.Sprintf("process:%s", sql.Plugin.Plugin)
name = sql.Plugin.Plugin
}
// この pair の処理をモニタリングする
packageRegion := trace.StartRegion(gctx, "package")
trace.Logf(gctx, "", "name=%s dir=%s plugin=%s", name, dir, lang)
// parse: SQL を解析する
result, failed := parse(gctx, name, dir, sql.SQL, combo, parseOpts, errout)
if failed {
packageRegion.End()
errored = true
return nil
}
// codegen: コードを生成する
out, resp, err := codegen(gctx, combo, sql, result)
if err != nil {
fmt.Fprintf(errout, "# package %s\n", name)
fmt.Fprintf(errout, "error generating code: %s\n", err)
errored = true
packageRegion.End()
return nil
}
files := map[string]string{}
// 生成されたコードを files マップに書き込む
for _, file := range resp.Files {
files[file.Name] = string(file.Contents)
}
// グローバルな output マップに生成されたコードを追加する
// 他の goroutine との競合を避けるために Mutex を使用する
m.Lock()
for n, source := range files {
filename := filepath.Join(dir, out, n)
output[filename] = source
}
m.Unlock()
packageRegion.End()
return nil
})
}
SQL の解析とコードの生成のロジックはそれぞれ parse
関数と codegen
関数に記述されているので、詳しく見ていく。
SQL の解析
parse 関数を見ていく。
コメントは筆者が挿入した。
func parse(ctx context.Context, name, dir string, sql config.SQL, combo config.CombinedSettings, parserOpts opts.Parser, stderr io.Writer) (*compiler.Result, bool) {
defer trace.StartRegion(ctx, "parse").End()
// SQL を解析するためのコンパイラを初期化する
// sql.Engine によって使用する parser/catalog が変わる
c := compiler.NewCompiler(sql, combo)
// スキーマの SQL を解析する
if err := c.ParseCatalog(sql.Schema); err != nil {
fmt.Fprintf(stderr, "# package %s\n", name)
if parserErr, ok := err.(*multierr.Error); ok {
for _, fileErr := range parserErr.Errs() {
printFileErr(stderr, dir, fileErr)
}
} else {
fmt.Fprintf(stderr, "error parsing schema: %s\n", err)
}
return nil, true
}
if parserOpts.Debug.DumpCatalog {
debug.Dump(c.Catalog())
}
// クエリの SQL を解析する
if err := c.ParseQueries(sql.Queries, parserOpts); err != nil {
fmt.Fprintf(stderr, "# package %s\n", name)
if parserErr, ok := err.(*multierr.Error); ok {
for _, fileErr := range parserErr.Errs() {
printFileErr(stderr, dir, fileErr)
}
} else {
fmt.Fprintf(stderr, "error parsing queries: %s\n", err)
}
return nil, true
}
return c.Result(), false
}
Parse について
クエリの形式はデータベースエンジンによって異なる。PostgreSQL 関連の処理は /internal/engine/postgresql 以下で internal の postgresql パッケージとして定義されている。
ParseCatalog や ParseQueries では /internal/engine/postgresql/parse.go で定義されている Parse 関数が呼ばれるので、それを読んでみる。
コメントは筆者が挿入した。
func (p *Parser) Parse(r io.Reader) ([]ast.Statement, error) {
// ここで SQL のスキーマやクエリを受け取る
contents, err := io.ReadAll(r)
if err != nil {
return nil, err
}
// SQL を解析して Parse tree を構築する
// nodes.Parse による解析では github.com/pganalyze/pg_query_go/v4 を使っている
// これにより解析結果の tree を得るので、後で最終的に Go のコードに変換するための AST に渡す
tree, err := nodes.Parse(string(contents))
if err != nil {
pErr := normalizeErr(err)
return nil, pErr
}
var stmts []ast.Statement
// 解析した SQL の各ステートメントに対してイテレートする
for _, raw := range tree.Stmts {
// translate 関数で sqlc がコードを生成するための AST を構築する
n, err := translate(raw.Stmt)
if err == errSkip {
continue
}
if err != nil {
return nil, err
}
if n == nil {
return nil, fmt.Errorf("unexpected nil node")
}
stmts = append(stmts, ast.Statement{
Raw: &ast.RawStmt{
Stmt: n,
StmtLocation: int(raw.StmtLocation),
StmtLen: int(raw.StmtLen),
},
})
}
return stmts, nil
}
translate 関数は非常に長いので読むのが大変だが、基本的には pganalyze/pg_query_go を使って解析された PostgreSQL のノードを sqlc 内の処理で使う AST に変換する責務を負っていると考えてよい。
sqlc 内の処理で使う AST は internal/sql/ast 以下に定義されている。
ParseCatalog について
Catalog はクエリを解析するために持つデータを集めた sqlc の内部的なデータ構造。PostgreSQL、MySQL、SQLite などデータベースエンジンごとに異なる Catalog を持つ。
PostgreSQL のスキーマ解析に使われる Catalog を生成する関数は以下で定義されている。引用はしないが、行数がすごいので一度見てみて欲しい。
Catalog では SQL から AST を構築するにあたり internal/sql/ast 以下に列挙されている SQL 文を表す構造体を使用する。例えば CREATE TABLE 文は crate_table_stmt.go で以下のように定義されている。
package ast
type CreateTableStmt struct {
IfNotExists bool
Name *TableName
Cols []*ColumnDef
ReferTable *TableName
Comment string
Inherits []*TableName
}
func (n *CreateTableStmt) Pos() int {
return 0
}
例えば CREATE TABLE 文で使われる IF NOT EXISTS
は IfNotExists
として構造体のメンバーとして定義されている。pganalyze/pg_query_go の処理結果として取得した node を translate 関数でこれらの構造体にマッピングして sqlc 内でコードを生成するための AST に変換する。
ParseQueries について
catalog と同様 SQL 文を表す構造体は internal/sql/ast に格納されている。例えば SELECT 文は select_stmt.go で以下のように定義されている。
package ast
type SelectStmt struct {
DistinctClause *List
IntoClause *IntoClause
TargetList *List
FromClause *List
WhereClause Node
GroupClause *List
HavingClause Node
WindowClause *List
ValuesLists *List
SortClause *List
LimitOffset Node
LimitCount Node
LockingClause *List
WithClause *WithClause
Op SetOperation
All bool
Larg *SelectStmt
Rarg *SelectStmt
}
func (n *SelectStmt) Pos() int {
return 0
}
SELECT 文(Stmt は Statement の略?)で使われる DISTINCT
や GROUP BY
、ORDER BY
などが構造体のメンバーとして定義されている。
コードの生成
次は codegen 関数を見ていく。
コメントは筆者が挿入した。
func codegen(ctx context.Context, combo config.CombinedSettings, sql outPair, result *compiler.Result) (string, *plugin.CodeGenResponse, error) {
// デバッグ用のトレースを設定する
defer trace.StartRegion(ctx, "codegen").End()
// Catalog などの設定を req オブジェクトにまとめる
req := codeGenRequest(result, combo)
var handler ext.Handler
var out string
// どの言語、方式でコード生成するかを選択する
switch {
case sql.Gen.Go != nil:
out = combo.Go.Out
handler = ext.HandleFunc(golang.Generate)
case sql.Gen.JSON != nil:
out = combo.JSON.Out
handler = ext.HandleFunc(json.Generate)
case sql.Plugin != nil:
out = sql.Plugin.Out
plug, err := findPlugin(combo.Global, sql.Plugin.Plugin)
if err != nil {
return "", nil, fmt.Errorf("plugin not found: %s", err)
}
// プラグインでコード生成する場合は Process か WASM かによってハンドラを設定する
switch {
case plug.Process != nil:
handler = &process.Runner{
Cmd: plug.Process.Cmd,
Env: plug.Env,
}
case plug.WASM != nil:
handler = &wasm.Runner{
URL: plug.WASM.URL,
SHA256: plug.WASM.SHA256,
Env: plug.Env,
}
default:
return "", nil, fmt.Errorf("unsupported plugin type")
}
// プラグインのオプションを JSON 形式に変換する
opts, err := convert.YAMLtoJSON(sql.Plugin.Options)
if err != nil {
return "", nil, fmt.Errorf("invalid plugin options")
}
req.PluginOptions = opts
default:
return "", nil, fmt.Errorf("missing language backend")
}
// 選択したハンドラを使用してコードを生成する
resp, err := handler.Generate(ctx, req)
return out, resp, err
}
最後に handler.Generate で呼び出される関数は handler の設定によって異なる。Go ではこの Generate 関数が呼ばれる。
上記の Generate 関数が呼ぶ generate 関数では text/template で template からファイルを生成する。Go 版は /internal/codegen/golang/templates 以下に書かれており、embed パッケージを使って埋め込まれている。
デフォルトのテンプレートは /internal/codegen/golang/templatess/template.tmpl で、これを見ると sqlc が生成する .go ファイルに酷似していることが分かる。
最終的には ExecuteTemplate を使ってデータ(&tctx)を templateName のテンプレートを介して bufio に書き込んでいる。
コメントは筆者が挿入した。
execute := func(name, templateName string) error {
imports := i.Imports(name)
replacedQueries := replaceConflictedArg(imports, queries)
var b bytes.Buffer
w := bufio.NewWriter(&b)
tctx.SourceName = name
tctx.GoQueries = replacedQueries
err := tmpl.ExecuteTemplate(w, templateName, &tctx) // ここでテンプレートに沿ってテキストを生成し w を介して bufio に書き込んでいる
w.Flush()
if err != nil {
return err
}
code, err := format.Source(b.Bytes())
if err != nil {
fmt.Println(b.String())
return fmt.Errorf("source error: %w", err)
}
if templateName == "queryFile" && golang.OutputFilesSuffix != "" {
name += golang.OutputFilesSuffix
}
if !strings.HasSuffix(name, ".go") {
name += ".go"
}
output[name] = string(code)
return nil
}
エラーの処理と生成されたコードの返却
最後はエラー処理して output を返すだけ。
if err := grp.Wait(); err != nil {
return nil, err
}
if errored {
for i, _ := range stderrs {
if _, err := io.Copy(stderr, &stderrs[i]); err != nil {
return nil, err
}
}
return nil, fmt.Errorf("errored")
}
return output, nil
まとめ
私自身まだ使い始めたばかりで把握していないことが多く自分自身が Go 初心者でもあるので、誤った認識や不足している記述があるかもしれない(あったらすみません)。
これから使っていく中で面白いことがあればまたブログに書いてみようと考えている。
sqlc
- SQL ファイルから型安全な Go のデータベースクライアントやスキーマと対応した構造体を簡単に生成できて便利
- 生成されるファイルを設定ファイルやプラグイン等で加工することができる
sqlc の導入については公式ドキュメントの他に以下のブログが参考になりそう。
プラグインについては以下のブログが参考になりそう。
sqlc generate コマンド
- SQL ファイルからの生成は SQL の句ごと、データベースエンジン別に AST を組み立てるための Go の構造体が用意されており、それを参照して SQL を解析する
- コードの生成は予め template が用意され embed されており、text/template を使ってデータを流し込む
Footnotes
-
Go 以外にも Kotlin や Python 等がサポートされている https://docs.sqlc.dev/en/stable/reference/language-support.html ↩