Skip to content

Instantly share code, notes, and snippets.

@utamori
Created October 30, 2020 01:57
Show Gist options
  • Save utamori/301210bfbad64cfad1094031f7de1e9d to your computer and use it in GitHub Desktop.
Save utamori/301210bfbad64cfad1094031f7de1e9d to your computer and use it in GitHub Desktop.

sqlc: A SQL Compiler

And lo, the Great One looked down upon the people and proclaimed:

"SQL is actually pretty great"

sqlc は、SQL から非常に型安全で使いやすい Go コードを生成します。以下にその方法を示します。 手順:

  1. SQL クエリを書きます
  2. sqlc を実行して、これらのクエリに型安全なインタフェースを提供する Go コードを生成します
  3. sqlc が生成したメソッドを呼び出すアプリケーションコードを書きます。

本当に、それはとても簡単です。もう二度と SQL クエリのボイラープレートコードを書く必要はありません。

Preventing Errors

sqlc は単にボイラプレートを生成して生産性を上げるだけではありません。 sqlc は SQL における一般的なエラーのほとんどを防止します

あなたは今までに次のようなことを経験したことはありませんか?

  • クエリを実行する際に引数の順番が間違っていたため、SQL 文とマッチしなかった
  • あるクエリではカラム名を更新したが、他のクエリで更新するのを忘れた
  • クエリ内のカラム名が間違っていた
  • クエリの引数の数を変更したが、呼び出す際に追加の値を渡すのを忘れた
  • カラムの型を変更したが、コードでは型を変更し忘れた

これらのエラーはすべて sqlc では 絶対にありえません 。待って、どういうこと?どうやって?

sqlc はコード生成プロセスの間にすべてのクエリと DDL 文(例: CREATE TABLE)を解析します。つまり、テーブル内のすべてのカラムの名前、型、クエリ内のすべての式を知っています。それらのどれかが一致しない場合、sqlc は クエリのコンパイル に失敗します。実行時エラーをコンパイル時に防ぐことができます。

同じく、sqlc が生成するメソッドは、厳密なアリティとカラムにマッチする正しい Go 型定義を持っています。ですから、クエリの引数やカラムの型を変更して、コードは更新していない場合、コンパイルに失敗します。

Getting Started

誇大広告はもう終わりにして 実際に見てみましょう。

まず、以下の SQL をsqlc generateコマンドに渡します:

CREATE TABLE authors (
  id   BIGSERIAL PRIMARY KEY,
  name text      NOT NULL,
  bio  text
);

-- 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: DeleteAuthor :exec
DELETE FROM authors
WHERE id = $1;

そして、以下のように、これを利用するコードを書きます

// authorsの一覧を取得
authors, err := db.ListAuthors(ctx)
if err != nil {
    return err
}
fmt.Println(authors)

// authorを作成
insertedAuthor, err := db.CreateAuthor(ctx, db.CreateAuthorParams{
        Name: "Brian Kernighan",
        Bio:  sql.NullString{String: "Co-author of The C Programming Language and The Go Programming Language", Valid: true},
})
if err != nil {
        return err
}
fmt.Println(insertedAuthor)

// 作成したauthorを取得する
fetchedAuthor, err := db.GetAuthor(ctx, insertedAuthor.ID)
if err != nil {
        return err
}
// trueが出力される
fmt.Println(reflect.DeepEqual(insertedAuthor, fetchedAuthor))

このコードを実行可能にするために、sqlc は読みやすく、使いやすい Go コードを生成します。見てみてください。

package db

import (
	"context"
	"database/sql"
)

type Author struct {
	ID   int64
	Name string
	Bio  sql.NullString
}

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
}

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,
	}
}

Examples

さまざまな PostgreSQL / Go の機能がサポートされています。

エンドツーエンドの完全なサンプルは、[ondeck] (./examples/ondeck) パッケージにあります

Usage

Usage:
  sqlc [command]

Available Commands:
  compile     Statically check SQL for syntax and type errors
  generate    Generate Go code from SQL
  help        Help about any command
  init        Create an empty sqlc.yaml settings file
  version     Print the sqlc version number

Flags:
  -h, --help   help for sqlc

Use "sqlc [command] --help" for more information about a command.

Settings

sqlc ツールは sqlc.yaml または sqlc.json を使って設定します。このファイルは sqlc コマンドを実行するディレクトリに置く必要があります。

version: "1"
packages:
  - name: "db"
    path: "internal/db"
    queries: "./sql/query/"
    schema: "./sql/schema/"
    engine: "postgresql"
    emit_json_tags: true
    emit_prepared_queries: true
    emit_interface: false
    emit_exact_table_names: false
    emit_empty_slices: false

packagesドキュメントには、以下のキーがあります。

  • name:
    • 生成コードに使用するパッケージ名。デフォルトは path ベースネーム
  • path:
    • 生成コードを出力するディレクトリ
  • queries:
    • SQL クエリのディレクトリ。または単一の SQL ファイルへのパス。またはパスのリスト
  • schema:
    • SQL スキーママイグレーションファイルのディレクトリ。または単一の SQL ファイルへのパス。またはパスのリスト
  • engine:
    • postgresql または mysql のいずれかを指定します。デフォルトは postgresql です。MySQL のサポートは実験的なものです。
  • emit_json_tags:
    • true の場合、生成された構造体に JSON タグを追加する。デフォルトは false
  • emit_prepared_queries:
    • true の場合、プリペアドクエリをサポートするようにする。デフォルトは false
  • emit_interface:
    • true の場合、生成されるパッケージに Querier インターフェースを出力する。デフォルトは false
  • emit_exact_table_names:
    • true の場合、構造体名はテーブル名を反映します。そうでない場合、sqlc は複数のテーブル名を単数化しようとします。デフォルトは false
  • emit_empty_slices:
    • true の場合:many クエリが返すスライスは nil ではなく空になります。デフォルトは false

Type Overrides

PostgreSQL タイプから Go タイプへのデフォルトのマッピングでは、必要な場合にのみ、標準ライブラリ以外のパッケージを使用します

たとえば、PostgreSQL のuuid型はgithub.com/google/uuidにマップされます UUID 用の別の Go パッケージが必要な場合は、overrides配列にパッケージを指定します。この場合、代わりに github.com/gofrs/uuidが使われます

version: "1"
packages: [...]
overrides:
  - go_type: "github.com/gofrs/uuid.UUID"
    db_type: "uuid"

overrridesドキュメントは以下のキーを持ちます:

  • db_type:
    • オーバーライドする PostgreSQL の型。サポートされている型の一覧は gen.goを参照してください
  • go_type:
    • 生成されたコードで使用する Go の型の完全修飾名。
  • nullable:
    • true の場合、この型はカラムがヌル可能な場合に使用される。デフォルトは false

Per-Column Type Overrides

前のセクションで説明したようにタイプベースではなく、テーブルの特定のフィールドに対して、モデルまたはクエリ生成で使用される Go のタイプをオーバーライドしたい場合があります。

これは、override 定義で column プロパティを指定することで設定できます。columntable.column の形式でなければなりませんが、schema.table.columncatalog.schema.table.column を指定することでさらに指定することができます。

version: "1"
packages: [...]
overrides:
  - column: "authors.id"
    go_type: "github.com/segmentio/ksuid.KSUID"

Package Level Overrides

前のセクションで説明したように、オーバーライドはグローバルに設定できます。 パッケージごとに設定することも可能で、オーバーライドの動作を単一のパッケージだけにスコープすることもできます。

version: "1"
packages:
  - overrides: [...]

Renaming Struct Fields

構造フィールド名は、以下のような単純なアルゴリズムを使用してカラム名から生成されます:

アンダースコアでカラム名を分割し、各部分の最初の文字を大文字にします。

account     -> Account
spotify_url -> SpotifyUrl
app_id      -> AppID

生成されたフィールド名に不満がある場合は、rename 辞書を使って新しい名前を選ぶことができます。キーはカラム名、値は使用する構造体フィールド名です

version: "1"
packages: [...]
rename:
  spotify_url: "SpotifyURL"

Installation

macOS

brew install kyleconroy/sqlc/sqlc

Ubuntu

sudo snap install sqlc

go get

go get github.com/kyleconroy/sqlc/cmd/sqlc

Docker

docker pull kjconroy/sqlc

Run sqlc using docker run:

docker run --rm -v $(pwd):/src -w /src kjconroy/sqlc generate

Downloads

Binaries for a given release can be downloaded from the stable channel on Equinox or the latest GitHub release.

Tip Releases

Each commit is deployed to the devel channel on Equinox:

Other Databases and Languages

sqlc は現在 PostgreSQL / Go のみをサポートしています。MySQL と Kotlin のサポートはマージされていますが、どちらも実験的なものとしてマークされています。SQLite と TypeScript のサポートが予定されています。

Language PostgreSQL MySQL
Go ✅ - Stable 🐛 - Beta
TypeScript ⏲️ - Planned ⏲️ - Planned
Kotlin ⚠️ - Experimental

他のデータベースや言語を追加したいなら、私たちは貢献を歓迎します。

Sponsors

sqlc development is funded by our generous sponsors.

If you use sqlc at your company, please consider becoming a sponsor today.

Sponsors receive priority support via the sqlc Slack organization.

Development

Building

For local development, install sqlc under an alias. We suggest sqlc-dev.

go build -o ~/go/bin/sqlc-dev ./cmd/sqlc

Running Tests

To run the tests, include the exp tag. Without this tag, a few tests will fail.

go test --tags=exp ./...

To run the tests in the examples folder, a running PostgreSQL instance is required. The tests use the following environment variables to connect to the database

For PostgreSQL

Variable     Default Value
-------------------------
PG_HOST      127.0.0.1
PG_PORT      5432
PG_USER      postgres
PG_PASSWORD  mysecretpassword
PG_DATABASE  dinotest

For MySQL

Variable     Default Value
-------------------------
MYSQL_HOST      127.0.0.1
MYSQL_PORT      3306
MYSQL_USER      root
MYSQL_ROOT_PASSWORD  mysecretpassword
MYSQL_DATABASE  dinotest
go test --tags=examples,exp ./...

Regenerate expected test output

If you need to update a large number of expected test output in the internal/endtoend/testdata directory, run the regenerate.sh script.

make regen

Note that this uses the sqlc-dev binary, not sqlc so make sure you have an up to date sqlc-dev binary.

Acknowledgements

sqlc was inspired by PugSQL and HugSQL.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment