sqlcでType Overrides の活用とgo-optionalの話

[前回](https://qiita.com/ktat/items/ddf0e5e510279503feee)、[前々回](https://qiita.com/ktat/items/2ed13afcd29f49aee6a1)に続き、sqlcの話です。もう少し続きます。

## Type Overrides

sqlcは、YAMLで設定を書きますが、`overrides`という項目があります([ドキュメント](https://docs.sqlc.dev/en/stable/howto/overrides.html))。これは何かというと、DBの型に対するGoの型(例えば、`VARCHAR`を`string`にする)のデフォルトを上書きできるというものです。

ちなみに、実際これを使わないと、MySQLの`DECIMAL`はGoの`string`になってしまいます。

### MySQLの`DECIMAL`にGoの型(`decimal.Decimal`)を指定する

MySQLの型をGoの型で置き換えるためには、sqlc.ymlの`overrides`に、以下のように書きます。

```yaml
            overrides:
                - db_type: decimal
                  go_type:
                    import: github.com/shopspring/decimal
                    type: Decimal
```

これで、mysqlの`decimal`は、`github.com/shopspring/decimal` の `Decimal`にマッピングされます。

null可のカラムなら、`nullable: true`をつけて、typeも、`NullDecimal`も定義しましょう。

```yaml
                - db_type: "decimal"
                  nullable: true
                  go_type:
                    import: "github.com/shopspring/decimal"
                    type: "NullDecimal"
```

### 特定のカラムにGoの型を指定する

先程は、DBの特定の型に対してGoの型を指定してoverrideしましたが、カラムを指定することも出来ます。

```yaml
            overrides:
                - column: hogehoge.id
                  go_type:
                    import: your-project/domain/valueobject
                    type: HogehogeID
```

`column`のところは、`テーブル名.カラム名` にしないといけません(試してないけど、`スキーマ名.テーブル名.カラム名`もいけるはず)。なお、`db_type`とは排他的ですので、同時に指定することは出来ません。

自前の型でoverrideすることで、たとえば、`TaskStatus` のような型でdefaultの`int8`をoverrideしてやれば、その型に`IsFinished` とか、`IsPlanned`のようなメソッドを用意しておくことも出来ます。

ただ、typeが構造体の場合は、`Scan`と`Value`を実装する必要があります。

### 型にScan/Valueを実装する

例えば、とあるカラムにJSONが入っていたとします。DBから取り出した時点でUnmarshalされていれば、とても便利ですね。そういう時は、自分で定義した型に`Scan`を実装します。

```go
package valueobject

import (
	_ "database/sql"
	"database/sql/driver"
	"encoding/json"
	"fmt"
	_ "unsafe"
)

type Config struct {
	Hoge struct {
		URL string `json:"url"`
	} `json:"hoge"`
}

func (c *Config) Scan(src any) error {
	if src == nil {
		*c = Config{}
		return nil
	}

	var v string
	err := sqlConvertAssign(&v, src)
	if err != nil {
		return err
	}
	data := &Config{}
	err = json.Unmarshal([]byte(v), &data)

	if err != nil || data == nil {
		*c = Config{}
		return nil
	}

	*c = *data

	return nil
}

func (c *MallConfig) Value() (driver.Value, error) {
	bytes, err := json.Marshal(c)
	if err != nil {
		return nil, err
	}
	return string(bytes), nil
}
```
`sqlConvertAssign` を別ファイルで定義しておくと良いです。

```go
package valueobject

import (
	_ "database/sql"
	_ "unsafe"
)

//go:linkname sqlConvertAssign database/sql.convertAssign
func sqlConvertAssign(dest, src any) error
```

## go-optional の話

最初に、`NullDecimal`が出てきましたが、`NullDecimal`や`sql.Null***`などを使うところを、@moznionさんの[go-optional](https://github.com/moznion/go-optional)で置き換えるととても良いです(ちなみに、前段の`Scan`と`Value`はgo-optionalのコードを参考にしていますというか、一部そのままです)。

go-optional は、Genericsを使ってGoでOption型を実現しています。

通常よく使われている、`sql.NullString`や、`sql.NullInt` は、`Valid` メソッドで値がnilかどうかを**チェックすることになっている**のですが、構造体のメンバに、`int`や`string`が入っているため、初期値として、`0`や`空文字`が入っています。要は、**チェックしなくても使えてしまいます**。

```go
hoge := sql.NullString{Valid:false, String:""}
if ! hoge.Valid() {
  // nil扱い
}
```
なんですけど、
```go
hoge := sql.NullString{Valid:false, String:""}
str := hoge.String 
```
と書けてしまうので、イマイチ感が拭えません。

overridesで `go-optional` を使ってみましょう。

```yaml
          - db_type: "string"
            nullable: true
            go_type:
              import: "github.com/moznion/go-optional"
              type: "Option[string]"
```

こうすることにより、下記のように書けます。

```go
str, err := hoge.Take()
if err != nil {
  // hogeはnil
}
```

もちろん、`Take()`を使わずに、`IsSome()`でチェックせずに、`Unwrap()`してしまう可能性はゼロではありませんが、`Take`を使うとチーム内で決めてしまえば、コードレビューで防げるレベルかと思います。

## 注意事項

`go_type` には、`import` が1つしか書けません。ので、go-optionalを使って、`type: Option[valueobject.YourType]`のようにした場合、`github.com/moznion/go-optional` と、`your-project/domain/valueobject` の2つをimportする必要があります。

その場合は、下記のようにして、overridesに、YourOptionType、うまく動きます。

```go
type YourOptionType = optional.Option[YourType]
```

この場合、YourOptionTypeに独自のメソッドを書くことは出来ません。当初デメリットかと思っていましたが、UnWrapすれば使えるので特に問題ないと思います。

ちなみに、下記の定義ではうまくいきません。

```go
type YourOptionType optional.Option[YourType]
```

**query/以下のファイルを分割しない場合は**、`Option[valueobject.*]` ではない、`valueobject.*`の型を一つでも定義しておけば、そこでimportはされるので、特に問題無いです。生成されるのは一つのファイルなので、必要なimportがoverridesの定義の中に1つでも現れていれば特に問題ないわけです。

「いや、全部 null可なんだけど...」という場合は、非常にダサい解決策として、ダミーのテーブルを用意して、そこに、null可ではない、独自型のカラムを適当に用意してやって、overridesしてやれば、なんとかなるとは思います。

別のworkaroundとして、`import`に改行付きで2つ書く(`import: "time"\n"github.com/myproject/null"`のように)...という手段が[あるようです](https://github.com/sqlc-dev/sqlc/issues/1662#issuecomment-1197934328)が、まともに動くのかな...?

## 終わり

sqlcのType Overridesと関連してgo-optionalの話でした。どちらもなかなか便利ですし、組み合わせると最高ですので、積極的に使っていくと良いのではないでしょうか。

ただ、いろんな場所で使われているカラムをoverridesで書いていくのは、すごく、だるいですね。テーブル増えたらどうすんねん...という気分になります。

そうですね、人間がやることではないです。自動化しましょう。[次回](https://qiita.com/ktat/items/75096d8f2c9014b99754)に続きます。