generated at
Denoでsqlcを使う
はじめに
Denoで以下の組み合わせを試してみたので内容をまとめておきます

前提
以下のバージョンで動作確認しています
Deno@v1.43.0
sqlc@v1.25.0

セットアップ
1. まず、sqlcの設定ファイルであるsqlc.yamlを用意します
sqlc.yaml
version: "2" sql: - schema: "schema.sql" queries: "query.sql" engine: postgresql codegen: - out: generated plugin: ts options: runtime: node driver: postgres plugins: - name: ts wasm: url: https://downloads.sqlc.dev/plugin/sqlc-gen-typescript_0.1.3.wasm sha256: 287df8f6cc06377d67ad5ba02c9e0f00c585509881434d15ea8bd9fc751a9368
重要なのは以下の部分です
以下により、sqlc-gen-typescriptを読み込むように設定されています (README.mdに最新バージョンのインストール方法が記載されているはずなので、最新のバージョンについてはそちらを参照ください🙇‍♂️)
yaml
plugins: - name: ts wasm: url: https://downloads.sqlc.dev/plugin/sqlc-gen-typescript_0.1.3.wasm sha256: 287df8f6cc06377d67ad5ba02c9e0f00c585509881434d15ea8bd9fc751a9368
以下によりPostgres.js向けのコードが生成されるよう設定されます
yaml
sql: - schema: "schema.sql" queries: "query.sql" engine: postgresql codegen: - out: generated plugin: ts options: runtime: node driver: postgres
sqlcがコードの生成に利用するスキーマの定義は schema.sql SQL query.sql からそれぞれ読み込まれます
2. schema.sql を用意します。このファイルにDDLを記述します
sql
CREATE TABLE users ( id BIGSERIAL PRIMARY KEY, name text NOT NULL );
3. psqlなどで schema.sql を実行して、テーブルを用意しておきます
shell
$ psql -f schema.sql "$DATABASE_URL"
4. query.sql を用意します。このファイルにDMLを記述します
query.sql
-- name: ListUsers :many SELECT * FROM users ORDER BY id; -- name: FindUserByID :one SELECT * FROM users WHERE id = $1 LIMIT 1; -- name: InsertUser :one INSERT INTO users (name) VALUES ($1) RETURNING *;
5. sqlc generate を実行すると、 generated ディレクトリにTypeScriptファイルが生成されます
shell
$ sqlc generate $ ls generated query_sql.ts
以下が生成されるファイルのイメージです
generated/query_sql.ts
import { Sql } from "postgres"; export const listUsersQuery = `-- name: ListUsers :many SELECT id, name FROM users ORDER BY id`; export interface ListUsersRow { id: string; name: string; } export async function listUsers(sql: Sql): Promise<ListUsersRow[]> { return (await sql.unsafe(listUsersQuery, []).values()).map(row => ({ id: row[0], name: row[1] })); } export const findUserByIDQuery = `-- name: FindUserByID :one SELECT id, name FROM users WHERE id = $1 LIMIT 1`; export interface FindUserByIDArgs { id: string; } export interface FindUserByIDRow { id: string; name: string; } export async function findUserByID(sql: Sql, args: FindUserByIDArgs): Promise<FindUserByIDRow | null> { const rows = await sql.unsafe(findUserByIDQuery, [args.id]).values(); if (rows.length !== 1) { return null; } const row = rows[0]; return { id: row[0], name: row[1] }; } export const insertUserQuery = `-- name: InsertUser :one INSERT INTO users (name) VALUES ($1) RETURNING id, name`; export interface InsertUserArgs { name: string; } export interface InsertUserRow { id: string; name: string; } export async function insertUser(sql: Sql, args: InsertUserArgs): Promise<InsertUserRow | null> { const rows = await sql.unsafe(insertUserQuery, [args.name]).values(); if (rows.length !== 1) { return null; } const row = rows[0]; return { id: row[0], name: row[1] }; }
6. deno.jsonImport mapsを定義します
deno.json
{ "imports": { "postgres": "npm:postgres@3.4.4" }, "exclude": ["generated/query_sql.ts"] }
以下の設定により、 postgres というspecifierが npm:postgres@3.4.4 として解釈されます (npmレジストリからPostgres.js v3.4.4 が読み込まれます)
deno.json
"postgres": "npm:postgres@3.4.4"
これにより、sqlcによって生成された generated/query_sql などで以下の読み込みが解決できます
typescript
import postgres from "postgres";
また、以下の設定により、sqlcによって生成されたTypeScriptファイルをdeno fmtdeno lintなどの対象から除外できます
deno.json
"exclude": ["generated/query_sql.ts"]
7. main.ts を用意します
main.ts
import assert from "node:assert/strict"; import postgres from "postgres"; import { findUserByID, insertUser, listUsers } from "./generated/query_sql.ts"; // DATABASE_URL環境変数を読み込みます // DATABASE_URLには以下のような値を設定しておきます (<username>〜<database>までの各値は適宜置き換えが必要です) // ``` // DATABASE_URL=postgres://<username>:<password>@<host>:<port>/<database> // ``` const databaseURL = Deno.env.get("DATABASE_URL"); assert(databaseURL, "`DATABASE_URL` is required"); // Postgres.jsとsqlcで自動生成されたコードを使い、クエリを実行します const sql = postgres(databaseURL); try { const foo = await insertUser(sql, { name: "foo" }); assert.equal(foo.name, "foo"); const found = await findUserByID(sql, { id: foo.id }); assert.deepEqual(foo, found); const users = await listUsers(sql); assert(Array.isArray(users)); assert.deepEqual(users.at(-1), foo); } finally { // コネクションを閉じます await sql.end(); }
8. main.ts を実行します (特にエラーがでなければOK🙆‍♂️)
shell
# DATABASE_URL環境変数が設定されている必要があるためご注意 $ deno run --allow-env --allow-net main.ts

本格的にやるなら
スキーマの管理について
このページでは単純な方法を紹介しましたが、本格的に利用するのであれば以下の方法なども検討するとよいかもしれません
schema.sql pg_dumpで生成する
sqlcdbmateなどの様々なマイグレーションツールにも対応しているため、これらのマイグレーションファイルを利用する

動的なクエリの生成について
sqlcで動的なクエリの生成が難しい場合は、必要に応じてkysely+kysely-postgres-jsあたりを適宜併用するのも手かもしれません

補足
Deno v2.1からdeno fmtコマンドに .sql ファイルのフォーマットがサポートされています
sqlcを使いたい場合に便利そうです

関連ページ