squirrel を試してみた。

squirrel

すごくシンプルな SQL 生成ツール。これぐらいだと気軽に使えて良い。

https://github.com/Masterminds/squirrel

例えば、こうすると。

package main

import (
    "fmt"

    sq "github.com/Masterminds/squirrel"
)

func main() {
    users := sq.Select("*").From("users").Join("emails USING (email_id)")
    active := users.Where(sq.Eq{"deleted_at": nil})
    sql, args, _ := active.ToSql()
    fmt.Println(sql, args)
}

こうなる。

$ go run main.go
SELECT * FROM users JOIN emails USING (email_id) WHERE deleted_at IS NULL []

具体的な値が指定された場合には、ちゃんと別にして返してくれる。賢い。

package main

import (
    "fmt"

    sq "github.com/Masterminds/squirrel"
)

func main() {
    users := sq.Select("*").From("users").Join("emails USING (email_id)")
    active := users.Where(sq.Eq{"deleted_at": 10})
    sql, args, _ := active.ToSql()
    fmt.Println(sql, args)
}
$ go run main.go
SELECT * FROM users JOIN emails USING (email_id) WHERE deleted_at = ? [10]

評価式も挿入できる。

package main

import (
    "fmt"

    sq "github.com/Masterminds/squirrel"
)

func main() {
    sql, args, _ := sq.Insert("users").Columns("name", "age").Values("moe", 13).Values("larry", sq.Expr("? + 5", 12)).ToSql()
    fmt.Println(sql, args)
}
$ go run main.go
INSERT INTO users (name,age) VALUES (?,?),(?,? + 5) [moe 13 larry 12]

それぞれの関数が文字列を受け付けているので、ネストした SQL も記述可能。 ただ、IN() のクエリにうまく対応していないのが玉にキズ。どうでもいいけど、PR も結構溜まっている。

https://github.com/Masterminds/squirrel/pulls

package main

import (
    "fmt"

    "github.com/Masterminds/squirrel"
)

func main() {
    sq1 := squirrel.Select("*").From("users")
    sql1, args1, _ := sq1.ToSql()
    inner := fmt.Sprintf("("+sql1+") as 'b' ON 'a'.id = 'b'.email_id", args1...)
    sq2 := squirrel.Select("*").From("users as 'a'").Join(inner)
    sql2, args2, _ := sq2.ToSql()
    fmt.Println(sql2, args2)
}
$ go run main.go
SELECT * FROM users as 'a' JOIN (SELECT * FROM users) as 'b' ON 'a'.id = 'b'.email_id []

その他、database を引数として渡すことで SQL をそのまま実行可能らしいので試してみます。 適当な sample schema をダウンロード。

$ wget https://www.mysqltutorial.org/wp-content/uploads/2018/03/mysqlsampledatabase.zip
$ unzip mysqlsampledatabase.zip

docker で mysql を立てて、import したら完了。中身も見てみる。

$ docker run -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root -d mysql:5.7
$ mysql -uroot -proot -h127.0.0.1 < ./mysqlsampledatabase.sql
$ mysql -uroot -proot -h127.0.0.1 -e 'use classicmodels; show tables;'
mysql: [Warning] Using a password on the command line interface can be insecure.
+-------------------------+
| Tables_in_classicmodels |
+-------------------------+
| customers               |
| employees               |
| offices                 |
| orderdetails            |
| orders                  |
| payments                |
| productlines            |
| products                |
+-------------------------+
package main

import (
    "database/sql"
    "fmt"

    "github.com/Masterminds/squirrel"

    _ "github.com/go-sql-driver/mysql"
)

func main() {
    db, err := sql.Open("mysql", "root:root@/classicmodels")
    if err != nil {
        panic(err)
    }
    sq := squirrel.Select("*").From("productlines")
    rows, _ := sq.RunWith(db).Query()
    for rows.Next() {
        product := ""
        text := ""
        var html sql.NullString
        var image sql.NullString
        _ = rows.Scan(&product, &text, &html, &image)
        fmt.Println(product, text)
    }
}

ちゃんと取れている。

$ go run main.go
Classic Cars Attention car enthusiasts: Make your wildest car ownership dreams come true. Whether you are looking for classic muscle cars, dream sports cars or movie-inspired miniatures, you will find great choices in this category. These replicas feature superb attention to detail and craftsmanship and offer features such as working steering system, opening forward compartment, opening rear trunk with
...

他には statement の cache 機能が存在する。string -> statement への変換処理を parser という構造体が担当しているが、この処理を cache している。 https://github.com/Masterminds/squirrel/blob/cd1fe0a38ba74020c727421ca469fc4a4d7f0258/stmtcacher.go#L39-L52

総じて、薄いながらも良くできている SQL Build ライブラリ。後で使ってみよう。