2024 年 12 月 25 日追記
sqlc と sqlc-gen-typescript は積極的なメンテナンスがされていないため、採用するときは注意してください。
sqlc とは Go で書かれた SQL を元にコードを生成するツール。
$ sqlc compilesqlc は MySQL や SQLite にも対応している。
sqlc は Wasm でプラグインが書けるようになってきており、つい最近 TypeScript 版がリリースされた。
現時点では、 PostgreSQL と MySQL のみ対応。
https://github.com/sqlc-dev/sqlc-gen-typescript
Python 向けの sqlc plugin もある。
https://github.com/sqlc-dev/sqlc-gen-python
sqlc にはマイグレーションはない。好きなのを使えばいいと思うが go-migrate をお勧めしたい。 dbmate も良いらしい。
利用したことはないが sqldef も気になってる。
動くコードを見て貰うのが良い
https://github.com/voluntas/sqlc-gen-ts-template
version:"2"plugins:-name: tswasm:url: https://downloads.sqlc.dev/plugin/sqlc-gen-typescript_0.1.3.wasmsha256: 287df8f6cc06377d67ad5ba02c9e0f00c585509881434d15ea8bd9fc751a9368# どちらか一方で良いsql:# こちらは pg 用の生成-schema: db/schema.sqlqueries: db/query/engine: postgresqlcodegen:-out: src/gen/sqlc/pgplugin: tsoptions:runtime: nodedriver: pg# こちらは postgres 用の生成-schema: db/schema.sqlqueries: db/query/engine: postgresqlcodegen:-out: src/gen/sqlc/postgresplugin: tsoptions:runtime: nodedriver: postgresimport{ Client}from"pg";import{ GenericContainer, Wait}from"testcontainers";import{ expect, test}from"vitest";import fsfrom"fs";import{ createAccount, deleteAccount, getAccount, listAccounts,}from"../src/gen/sqlc/account_sql";test("account",async()=>{// PostgreSQL コンテナを起動const container=awaitnewGenericContainer("postgres:latest").withEnvironment({POSTGRES_DB:"testdb",POSTGRES_USER:"user",POSTGRES_PASSWORD:"password",}).withExposedPorts(5432)// TCPポートが利用可能になるまで待機.withWaitStrategy(Wait.forListeningPorts()).start();// postgres クライアントの設定const client=newClient({ host: container.getHost(), port: container.getMappedPort(5432), database:"testdb", user:"user", password:"password",});await client.connect();// データベースへの ping (接続テスト)await client.query("SELECT 1");// ファイルを読み込んでSQL文を取得const schemaSQL= fs.readFileSync("db/schema.sql","utf-8");// スキーマの初期化await client.query(schemaSQL);awaitcreateAccount(client,{ id:"spam", displayName:"Egg", email:"ham@example.com",});const account=awaitgetAccount(client,{ id:"spam"});expect(account).not.toBeNull();// ここダサい、なんかいい書き方 Vitest にありそうif(account){expect(account.id).toBe("spam");expect(account.displayName).toBe("Egg");expect(account.email).toBe("ham@example.com");}awaitdeleteAccount(client,{ id:"spam"});const accounts=awaitlistAccounts(client);expect(accounts.length).toBe(0);await client.end();// コンテナを停止await container.stop();},30_000);-- name: GetAccount :oneSELECT*FROM accountWHERE id=@id;-- name: ListAccounts :manySELECT*FROM account;-- name: CreateAccount :execINSERTINTO account(id, display_name, email)VALUES(@id,@display_name,@email);-- name: UpdateAccountDisplayName :oneUPDATE accountSET display_name=@display_nameWHERE id=@idRETURNING*;-- name: DeleteAccount :execDELETEFROM accountWHERE id=@id;import{ QueryArrayConfig, QueryArrayResult}from"pg";interfaceClient{query:(config: QueryArrayConfig)=>Promise<QueryArrayResult>;}exportconst getAccountQuery=`-- name: GetAccount :oneSELECT pk, id, display_name, email, created_atFROM accountWHERE id = $1`;exportinterfaceGetAccountArgs{ id:string;}exportinterfaceGetAccountRow{ pk:number; id:string; displayName:string; email:string|null; createdAt: Date;}exportasyncfunctiongetAccount( client: Client, args: GetAccountArgs):Promise<GetAccountRow|null>{const result=await client.query({ text: getAccountQuery, values:[args.id], rowMode:"array",});if(result.rows.length!==1){returnnull;}const row= result.rows[0];return{ pk: row[0], id: row[1], displayName: row[2], email: row[3], createdAt: row[4],};}exportconst listAccountsQuery=`-- name: ListAccounts :manySELECT pk, id, display_name, email, created_atFROM account`;exportinterfaceListAccountsRow{ pk:number; id:string; displayName:string; email:string|null; createdAt: Date;}exportasyncfunctionlistAccounts(client: Client):Promise<ListAccountsRow[]>{const result=await client.query({ text: listAccountsQuery, values:[], rowMode:"array",});return result.rows.map((row)=>{return{ pk: row[0], id: row[1], displayName: row[2], email: row[3], createdAt: row[4],};});}exportconst createAccountQuery=`-- name: CreateAccount :execINSERT INTO account (id, display_name, email)VALUES ($1, $2, $3)`;exportinterfaceCreateAccountArgs{ id:string; displayName:string; email:string|null;}exportasyncfunctioncreateAccount( client: Client, args: CreateAccountArgs):Promise<void>{await client.query({ text: createAccountQuery, values:[args.id, args.displayName, args.email], rowMode:"array",});}exportconst updateAccountDisplayNameQuery=`-- name: UpdateAccountDisplayName :oneUPDATE accountSET display_name = $1WHERE id = $2RETURNING pk, id, display_name, email, created_at`;exportinterfaceUpdateAccountDisplayNameArgs{ displayName:string; id:string;}exportinterfaceUpdateAccountDisplayNameRow{ pk:number; id:string; displayName:string; email:string|null; createdAt: Date;}exportasyncfunctionupdateAccountDisplayName( client: Client, args: UpdateAccountDisplayNameArgs):Promise<UpdateAccountDisplayNameRow|null>{const result=await client.query({ text: updateAccountDisplayNameQuery, values:[args.displayName, args.id], rowMode:"array",});if(result.rows.length!==1){returnnull;}const row= result.rows[0];return{ pk: row[0], id: row[1], displayName: row[2], email: row[3], createdAt: row[4],};}exportconst deleteAccountQuery=`-- name: DeleteAccount :execDELETE FROM accountWHERE id = $1`;exportinterfaceDeleteAccountArgs{ id:string;}exportasyncfunctiondeleteAccount( client: Client, args: DeleteAccountArgs):Promise<void>{await client.query({ text: deleteAccountQuery, values:[args.id], rowMode:"array",});}