goでmysqlにDATETIMEを入れるときにgo-mysql-driverとbunでタイムゾーンの扱いが違う

goのTimeは日時とタイムゾーンの情報を持っている。日本標準時のタイムゾーンを持ったTimeを作ってみる。

location, err := time.LoadLocation("Asia/Tokyo")
if err != nil {
    log.Fatal(err)
}
t := time.Date(2019, 1, 2, 3, 4, 5, 0, location) // 2019年1月2日3時4分5秒

これをmysqlに突っ込む。mysqlとのコネクションはこんな感じ。

cfg := mysql.Config{
    User:      "root",
    Passwd:    "password",
    Net:       "tcp",
    Addr:      "127.0.0.1:4306",
    DBName:    "time",
    ParseTime: true,
}

Locは指定していない。その場合UTCになる。

https://github.com/go-sql-driver/mysql/blob/191a7c4c519ef60cf3e8656fde8728eee9194308/dsn.go#L73

// NewConfig creates a new Config and sets default values.
func NewConfig() *Config {
    return &Config{
        Collation:            defaultCollation,
        Loc:                  time.UTC,
        MaxAllowedPacket:     defaultMaxAllowedPacket,
        Logger:               defaultLogger,
        AllowNativePasswords: true,
        CheckConnLiveness:    true,
    }
}

go-mysql-driverで生のSQLを書いてmysqlにINSERTする

db.Exec("INSERT INTO `time` (`id`, `time`) VALUES (?, ?);", "Asia/Tokyo 2019-01-02T03:04:05", t)

このとき発行されるクエリをgeneral_logで確認すると、この段階でUTCの 2019-01-01 18:04:05 に変換されている。

2023-05-06T14:33:27.581647Z     9 Execute   INSERT INTO `time` (`id`, `time`) VALUES ('Asia/Tokyo 2019-01-02T03:04:05', '2019-01-01 18:04:05')

これはgo-mysql-driverがTimeをシリアライズする前にInでタイムゾーンをUTCに変換しているからだ。

https://github.com/go-sql-driver/mysql/blob/191a7c4c519ef60cf3e8656fde8728eee9194308/packets.go#L1119

b, err = appendDateTime(b, v.In(mc.cfg.Loc))

ではbunでTimeをINSERTするとどうなるか

type BTime struct {
    bun.BaseModel `bun:"time2"`
    ID            string
    Time          time.Time
}
bundb.NewInsert().Model(&BTime{ID: "BUN Asia/Tokyo 2019-01-02T03:04:05", Time: t}).Exec(ctx)

このとき発行されるクエリでは、Timeに設定されているタイムゾーンを無視して 2019-01-02 03:04:05 とシリアライズしている。

2023-05-06T14:41:39.343913Z    10 Query INSERT INTO `time2` (`id`, `time`) VALUES ('BUN Asia/Tokyo 2019-01-02T03:04:05', '2019-01-02 03:04:05')

どうやらこれは意図的な変更の結果らしい。mysqlやgo-mysql-driverなどと二重変換してしまって正常に動作しないという問題があったようだ(←わかってない)。

https://github.com/uptrace/bun/issues/168

とにかく、じゃあどうやれば安心してDATETIMEを扱えるんだよという話になるんだけど、mysqlとまたがる範囲で暗黙的な変換が入ると状態の把握が難しくなるので、goのアプリケーション側で確実にUTCにしちゃってからORMなりクエリビルダーなりに放り込むことにした。そうすれば少なくともバグったときにfmt.Printfでなんとかなる。

goとmysqlの間で苦しんでる人はたくさんいた(類似記事が多い)がbunの話してる人は全然いなかったのでテキトーに書いてみた。

おまけ dockerのmysqlでgeneral_logを見る

適当なファイルに以下を書いておいて

[mysqld]
general_log=1

そのファイルが入ったディレクトリを、コンテナの/etc/mysql/conf.dにマウントする

-v /path/to/cnf-dir/:/etc/mysql/conf.d

コンテナ起動後にコンテナに入ってファイルの場所を探してtailする

nerdctl exec -it <container_id> /bin/sh
mysql -u root --host 127.0.0.1 -p
> SHOW VARIABLES LIKE '%general_log%';
> exit;
tail /path/to/general.log

なんで general_log_file 使わないの?

なんか general_log_file=/var/log/mysql/general.log するとmysqlの設定値はそこになるんだけどファイルが作られないんだよね。パーミッションの問題とかあるのかな。

上記の方法でやるとログファイルは /var/lib/mysql/hoge.log に生える。