Movatterモバイル変換


[0]ホーム

URL:


時雨堂ノート時雨堂ノート
時雨堂ノートPublicationへの投稿
🤖

sqlc を TypeScript で利用する

に公開
2024/12/25
!

2024 年 12 月 25 日追記
sqlc と sqlc-gen-typescript は積極的なメンテナンスがされていないため、採用するときは注意してください。

まとめ

  • sqlc-gen-typescript かなり良い
  • 自分が TypeScript でウェブアプリを利用するなら間違いなく sqlc を選択する
  • SQL は共通言語という点で本当に偉大

sqlc とは

sqlc とは Go で書かれた SQL を元にコードを生成するツール。

$ sqlc compile

なぜ sqlc ?

  • 結局、それぞれの ORM 固有の技術を覚えるくらいなら SQL を覚えた方が早い
  • 拡張に ORM が対応していようがいまいが関係ない
  • SQL パーサーがlibpg_query という実際の PostgreSQL サーバーソースを使用している

sqlc は PostgreSQL だけなの?

sqlc は MySQL や SQLite にも対応している。

sqlc は Go だけなの?

sqlc は Wasm でプラグインが書けるようになってきており、つい最近 TypeScript 版がリリースされた。
現時点では、 PostgreSQL と MySQL のみ対応。

https://github.com/sqlc-dev/sqlc-gen-typescript

Python

Python 向けの sqlc plugin もある。

https://github.com/sqlc-dev/sqlc-gen-python

マイグレーションは?

sqlc にはマイグレーションはない。好きなのを使えばいいと思うが go-migrate をお勧めしたい。 dbmate も良いらしい。
利用したことはないが sqldef も気になってる。

参考コード

動くコードを見て貰うのが良い

https://github.com/voluntas/sqlc-gen-ts-template

  • sqlc-gen-typescript を利用して TypeScript コードを src/gen/sqlc/pg と src/gen/sqlc/postgres 以下に生成
  • Vitesttestcontainers を利用してモック無しでの E2E テスト
  • db 以下に query と schema の SQL を配置
  • sqlc.yaml にて利用する sqlc-gen-typescript の Wasm を指定
  • PostgreSQL のドライバーはpgpostgres を提供
    • 特にこだわりが無ければ pg を利用することをお勧めする
  • GitHub Actions でのテスト
sqlc.yaml
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: postgres
test/sqlc_pg.test.ts
import{ 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);
db/query/account.sql
-- 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;
src/gen/sqlc/pg/account_sql.ts
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",});}
時雨堂ノート により固定

時雨堂の商用製品

voluntas

時雨堂

Discussion


[8]ページ先頭

©2009-2025 Movatter.jp