🐘

sqldef を Go のアプリケーションに組み込む

2022/12/18に公開3

これは Go Advent Calendar 2022 の 18 日目の記事です。

はじめに

sqldef は SQL で羃等に DB スキーマ管理ができるツールです。入力された DDL ファイルと実際の DB のスキーマを比較して必要な DDL を自動で生成・実行してくれます。

sqldef は CLI ツールとして提供されており、通常は DB に接続可能なサーバーにバイナリを配置して実行します。 sqldef をラップした npm ライブラリも提供されており、 Node.js のアプリケーションに組み込んで使うこともできます。また WebAssembly ライブラリも用意されています。

sqldef を Go のアプリケーションに組み込む

sqldef は Go で書かれています。また、一般的な Go ツールと同じように main パッケージと実装のパッケージが分離されています。そのため、公式の README などに記述はありませんが、ライブラリとして Go のアプリケーションに組み込むことができます。

実際に PostgreSQL で動作するコードを記載します。

import (
	"errors"
	"fmt"
	"log"
	"net/url"
	"os"
	"strconv"

	"github.com/k0kubun/sqldef"
	"github.com/k0kubun/sqldef/database"
	"github.com/k0kubun/sqldef/database/postgres"
	"github.com/k0kubun/sqldef/parser"
	"github.com/k0kubun/sqldef/schema"
)

func migrate(databaseURL, schemaFile string) error {
	u, err := url.Parse(databaseURL)
	if err != nil {
		return fmt.Errorf("failed to parse the database url: %w", err)
	}
	password, ok := u.User.Password()
	if !ok {
		return fmt.Errorf("failed to get password from the database url: %s", u.User.String())
	}
	port, err := strconv.Atoi(u.Port())
	if err != nil {
		if errors.Is(err, strconv.ErrSyntax) {
			port = 5432
		} else {
			return fmt.Errorf("failed to convert port in the database url: %w", err)
		}
	}
	db, err := postgres.NewDatabase(database.Config{
		DbName:   u.Path[1:],
		User:     u.User.Username(),
		Password: password,
		Host:     u.Hostname(),
		Port:     port,
	})
	if err != nil {
		return fmt.Errorf("failed to create a database adapter: %w", err)
	}
	sqlParser := database.NewParser(parser.ParserModePostgres)
	desiredDDLs, err := sqldef.ReadFile(schemaFile)
	if err != nil {
		return fmt.Errorf("Failed to read %s: %w", schemaFile, err)
	}
	options := &sqldef.Options{DesiredDDLs: desiredDDLs}
	if u.Hostname() == "localhost" {
		os.Setenv("PGSSLMODE", "disable")
	}
	sqldef.Run(schema.GeneratorModePostgres, db, sqlParser, options)
}

基本的には sqldef の PostgreSQL 向け CLI である psqldef の main パッケージと同じ形です。アプリケーション起動時にこの関数を呼び出すことでマイグレーションを行うことができます。

おわりに

小規模なシステムではありますが、このコードは実際の本番環境で運用しています。 sqldef 自体はまだメジャーバージョン 0 なのでまれに破壊的は変更が入ることがあり、その変更には追従する必要がありますが、それ以外では特に大きな問題もなく運用できています。

Discussion

Masato IkedaMasato Ikeda

大変有用な記事をありがとうございます!
こちらの記事のコードを参考に実装して Heroku で実行してみたところ、次のようなエラーが起きました。

found syntax error when parsing DDL "CREATE EXTENSION "pg_stat_statements"": syntax error at position 17 near 'EXTENSION'

どうやら CREATE EXTENSION された PostgreSQL に対して実行する場合には、 psqldef の --skip-extension オプションに該当する SkipExtension: truedatabase.Config で指定する必要があるようです。

db, err := postgres.NewDatabase(database.Config{
		DbName:        u.Path[1:],
		User:          u.User.Username(),
		Password:      password,
		Host:          u.Hostname(),
		Port:          port,
		SkipExtension: true,
	})

(もしかしたらスキーマ定義ファイルに CREATE EXTENSION 自体も記述していれば回避できるのかもしれません。ただそこまでは試していません 🙄)

tchssktchssk

コメントありがとうございます。

記事執筆当時はなかったオプションのようです。他に --skip-view というのも追加されていました。

VIEW や EXTENSION を使わないユースケースでは今の記述で問題ないと思いますので、一旦記事本文はこのままにできればと思います 🙇

もしかしたらスキーマ定義ファイルに CREATE EXTENSION 自体も記述していれば回避できるのかもしれません。

見てみたら既に CREATE EXTENSION のサポートが入っていました。 sqldef どんどん進化していてすごいですね。

Masato IkedaMasato Ikeda

あっ、もし訂正求めているように受け取られたならすみません。こういうケースがあったよ、ぐらいの気持ちでコメントしました。

VIEW や EXTENSION を使わないユースケースでは今の記述で問題ないと思いますので、一旦記事本文はこのままにできればと思います 🙇

はい、それで全然問題ありません 🙋

見てみたら既に CREATE EXTENSION のサポートが入っていました。 sqldef どんどん進化していてすごいですね。

おおっ、そうなんですね。では CREATE EXTENSION を書いても回避できそうですね。調べていただいてありがとうございます!