Open9

「sqlc で D1 向けの TypeScript コードを生成する」という記事を書くためのメモ

voluntasvoluntas

sqlc とは

https://sqlc.dev/

  • Go で書かれた SQL コンパイルして型チェックをしてくれ、Go のコードを生成してくれるツール
  • PostgreSQL や MySQL 、SQLite に対応している
  • プラグインを Wasm で書くことができる
    • sqlc が wasmtime を内蔵している
    • Python や Kotlin のコードも出力ができる

Cloudflare D1 とは

https://developers.cloudflare.com/d1

sqlc-gen-ts-d1 とは

  • orisano が開発している sqlc プラグイン
  • Go で書かれており tinygo で Wasm 化している
  • Cloudflare D1 向けの TypeScript コードを出力できる

https://github.com/orisano/sqlc-gen-ts-d1

作者の orisano は sqlc 自体にかなり貢献をしている、sqlc チョットデキル 。

voluntasvoluntas

仕様

自分が Gist に公開していたのを参考にしてくれている。実際に生成されるコードを見て貰うのが早い。

voluntasvoluntas

sqlc.embed

  • sqlc.embed は sqlc 1.18.0 で入った account.* と org.* のときに、うまいことまとめてくれる機能
-- name: GetOrgAccount :one
SELECT sqlc.embed(account),
  sqlc.embed(org)
FROM account
  JOIN org_account ON account.pk = org_account.account_pk
  JOIN org ON org_account.org_pk = org.pk
WHERE org.id = @org_id
  AND account.id = @account_id;
const getOrgAccountQuery = `-- name: GetOrgAccount :one
SELECT account.pk AS account_pk, account.id AS account_id, account.display_name AS account_display_name, account.email AS account_email,
  org.pk AS org_pk, org.id AS org_id, org.display_name AS org_display_name
FROM account
  JOIN org_account ON account.pk = org_account.account_pk
  JOIN org ON org_account.org_pk = org.pk
WHERE org.id = ?1
  AND account.id = ?2`;

export type GetOrgAccountParams = {
  orgId: string;
  accountId: string;
};

export type GetOrgAccountRow = {
  account: Account;
  org: Org;
};

type RawGetOrgAccountRow = {
  account_pk: number;
  account_id: string;
  account_display_name: string;
  account_email: string | null;
  org_pk: number;
  org_id: string;
  org_display_name: string;
};

export async function getOrgAccount(
  d1: D1Database,
  args: GetOrgAccountParams
): Promise<GetOrgAccountRow | null> {
  return await d1
    .prepare(getOrgAccountQuery)
    .bind(args.orgId, args.accountId)
    .first<RawGetOrgAccountRow | null>()
    .then((raw: RawGetOrgAccountRow | null) => raw ? {
      account: {
        pk: raw.account_pk,
        id: raw.account_id,
        displayName: raw.account_display_name,
        email: raw.account_email,
      },
      org: {
        pk: raw.org_pk,
        id: raw.org_id,
        displayName: raw.org_display_name,
      },
    } : null);
}
voluntasvoluntas

戻り値

  • 基本的に D1Result が戻ってくる
const listAccountsQuery = `-- name: ListAccounts :many
SELECT pk, id, display_name, email
FROM account`;

export type ListAccountsRow = {
  pk: number;
  id: string;
  displayName: string;
  email: string | null;
};

type RawListAccountsRow = {
  pk: number;
  id: string;
  display_name: string;
  email: string | null;
};

export async function listAccounts(
  d1: D1Database
): Promise<D1Result<ListAccountsRow>> {
  return await d1
    .prepare(listAccountsQuery)
    .all<RawListAccountsRow>()
    .then((r: D1Result<RawListAccountsRow>) => { return {
      ...r,
      results: r.results ? r.results.map((raw: RawListAccountsRow) => { return {
        pk: raw.pk,
        id: raw.id,
        displayName: raw.display_name,
        email: raw.email,
      }}) : undefined,
    }});
}
voluntasvoluntas

D1 の仕様

  • run() は sqlc でいう exec 向け
  • first() は sqlc でいう one 向け
  • all() は sqlc でいう many 向け

バッチ

  • env.D1_TEST.batch
voluntasvoluntas

sqlc-gen-ts-d1 の使い方

sqlc の最新版をインストールする。 サクッと入る。

$ go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest

sqlc.yaml

version: "2"
plugins:
  - name: typescript-d1
    wasm:
      url: "https://github.com/orisano/sqlc-gen-typescript-d1/releases/download/v0.0.0-a/sqlc-gen-typescript-d1.wasm"
      sha256: "4a3ac1e4faeefbb8d85d2153ec82e6977db1aee6ddab641511c0bcdabd0a872f"
sql:
  - schema: db/schema.sql
    queries: db/query/
    engine: sqlite
    codegen:
      - out: src/gen/sqlc
        plugin: typescript-d1
        options: workers-types=2022-11-30

sqlc-gen-ts-d1 自前ビルド

バイナリ公開してくれてるので、ビルドしなくて良い

  • tinygo が必要
    • brew install tinygo
$ git clone https://github.com/orisano/sqlc-gen-ts-d1
$ cd sqlc-gen-ts-d1
$ make
  • bin/sqlc-gen-ts-d1.wasm
    • bin/ 以下にバイナリとハッシュが生成される
  • bin/sqlc-gen-ts-d1.wasm.sha256
    • このハッシュを sqlc.json に設定する
voluntasvoluntas

D1

d1 create

  • spam という DB を作る
$ pnpm wrangler d1 create spam --experimental-backend --location apac
Options:
      --location              A hint for the primary location of the new DB. Options:
                              weur: Western Europe
                              eeur: Eastern Europe
                              apac: Asia Pacific
                              wnam: Western North America
                              enam: Eastern North America
  [string]
      --experimental-backend  Use new experimental DB backend  [boolean] [default: false]
voluntasvoluntas

batch

現在は実験的で仕様を入れてみてる

既存のクエリー実行用に生成された関数で .batch() を呼び出すことで batch 実行時に評価されるようにした。この書き方が良いかどうかはわからないが、わかりやすくするための仕組み。

env.D1.batch([db.deleteAccount(..).batch(), db.createAccount(..).batcn()]);

そもそも batch 自体が特殊。早く TX 来てほしい。