sqlc — SQL jadi kode bertipe
sqlc membaca skema dan berkas kueri SQL-mu, lalu menghasilkan kode Go: struct hasil, parameter bertipe, dan fungsi siap panggil. Kamu tetap menulis SQL — yang hilang cuma boilerplate dan salah ketik.
Intisari
- Alur kerjanya:
schema.sql+query.sql→sqlc generate→ kode Go bertipe. - Salah nama kolom, salah jumlah parameter, dan salah tipe jadi error saat generate — bukan panic di produksi.
- Tanpa refleksi dan tanpa biaya runtime: keluarannya kode yang bisa kamu baca dan langkahi di debugger.
- Kode hasil generate di-commit, dan CI memastikan ia sinkron dengan berkas SQL.
- Batasnya: kueri yang bentuknya berubah-ubah (filter opsional) tetap butuh query builder atau SQL manual.
Alur kerjanya
db/
├── schema.sql # CREATE TABLE — sumber kebenaran struktur
├── query.sql # kueri, dengan anotasi -- name:
└── sqlc.yaml # konfigurasi
↓ sqlc generate
internal/db/ # DIGENERATE — jangan diedit tangan
├── db.go
├── models.go # struct dari schema.sql
└── query.sql.go # fungsi dari query.sql
# sqlc.yaml
version: "2"
sql:
- engine: "postgresql"
schema: "db/schema.sql"
queries: "db/query.sql"
gen:
go:
package: "db"
out: "internal/db"
sql_package: "pgx/v5"
emit_json_tags: true
emit_pointers_for_null_types: true
Menulis kueri
-- db/query.sql
-- name: AmbilProduk :one
SELECT id, nama, harga, dibuat
FROM produk
WHERE id = $1 AND dihapus_pada IS NULL;
-- name: CariProduk :many
SELECT id, nama, harga
FROM produk
WHERE nama ILIKE '%' || $1 || '%'
AND dihapus_pada IS NULL
ORDER BY harga
LIMIT $2 OFFSET $3;
-- name: BuatProduk :one
INSERT INTO produk (nama, harga)
VALUES ($1, $2)
RETURNING id, dibuat;
-- name: HapusProduk :exec
UPDATE produk SET dihapus_pada = now() WHERE id = $1;
-- name: HitungProduk :one
SELECT count(*) FROM produk WHERE dihapus_pada IS NULL;
| Anotasi | Menghasilkan |
|---|---|
:one | (T, error) — pgx.ErrNoRows kalau kosong |
:many | ([]T, error) |
:exec | error |
:execrows | (int64, error) — jumlah baris terpengaruh |
:batchexec | Versi batch pgx |
:copyfrom | CopyFrom untuk muat massal |
Yang dihasilkannya
// internal/db/query.sql.go — DIGENERATE
type CariProdukParams struct {
Column1 pgtype.Text `json:"column1"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
type CariProdukRow struct {
ID int64 `json:"id"`
Nama string `json:"nama"`
Harga int64 `json:"harga"`
}
func (q *Queries) CariProduk(ctx context.Context, arg CariProdukParams) ([]CariProdukRow, error)
// Pemakaian di repository-mu
type Repo struct{ q *db.Queries }
func NewRepo(pool *pgxpool.Pool) *Repo {
return &Repo{q: db.New(pool)}
}
func (r *Repo) Cari(ctx context.Context, kata string, batas, offset int32) ([]produk.Produk, error) {
baris, err := r.q.CariProduk(ctx, db.CariProdukParams{
Column1: pgtype.Text{String: kata, Valid: true},
Limit: batas,
Offset: offset,
})
if err != nil {
return nil, fmt.Errorf("cari produk: %w", err)
}
// Petakan tipe DB ke tipe domain — jangan biarkan tipe generate
// bocor ke seluruh aplikasi.
hasil := make([]produk.Produk, 0, len(baris))
for _, b := range baris {
hasil = append(hasil, produk.Produk{ID: b.ID, Nama: b.Nama, Harga: b.Harga})
}
return hasil, nil
}
Beri nama parameter supaya kodenya terbaca. Column1 muncul karena kueri di atas
memakai $1 di dalam ekspresi. Tulis sqlc.arg(kata) atau
@kata di SQL dan hasilnya jadi field bernama Kata:
WHERE nama ILIKE '%' || sqlc.arg(kata) || '%'
LIMIT sqlc.arg(batas) OFFSET sqlc.arg(lewati);
Transaksi
func (r *Repo) BuatPesanan(ctx context.Context, p Pesanan) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
q := r.q.WithTx(tx) // Queries yang sama, terikat ke transaksi ini
id, err := q.BuatPesanan(ctx, ...)
if err != nil {
return err
}
for _, it := range p.Item {
if err := q.KurangiStok(ctx, db.KurangiStokParams{...}); err != nil {
return err
}
}
return tx.Commit(ctx)
}
Menjaganya tetap sinkron
# Pasang lewat direktif tool di go.mod — versinya ikut tercatat (Fase 0)
go get -tool github.com/sqlc-dev/sqlc/cmd/sqlc
go tool sqlc generate
go tool sqlc vet # linter kueri, termasuk EXPLAIN kalau dikonfigurasi
# Langkah CI: gagal kalau ada yang lupa generate ulang
go tool sqlc generate
git diff --exit-code internal/db
Commit kode hasil generate. Alternatifnya — generate saat build — membuat go build
di laptop orang baru gagal sampai ia memasang sqlc, dan membuat kode tidak bisa dibaca di GitHub. Yang
di-commit tetap dijaga benar oleh langkah CI di atas.
Kapan sqlc bukan jawabannya
| Kasus | Alternatif |
|---|---|
| Filter opsional yang jumlahnya berubah-ubah | Query builder (squirrel), atau kueri dengan ($1::text IS NULL OR nama = $1) |
| Nama tabel dinamis (multi-tenant per skema) | SQL manual dengan daftar putih |
| Pencarian dengan banyak kombinasi pengurutan | Beberapa kueri bernama, satu per kombinasi yang benar-benar dipakai |
| Prototipe yang skemanya berubah tiap jam | SQL manual dulu; masuk sqlc setelah stabil |
Perbandingan singkat pendekatan lapisan data di Go:
database/sql/ pgx manual — kendali penuh, boilerplate paling banyak, salah ketik kolom baru ketahuan saat runtime.- sqlc — SQL tetap SQL, bertipe, tanpa biaya runtime. Pilihan default roadmap ini.
- GORM — paling cepat untuk CRUD, menyembunyikan SQL, punya biaya refleksi dan kejutan performa (materi berikutnya).
Latihan: siapkan schema.sql dan empat kueri untuk tabel produk,
jalankan sqlc generate, dan panggil dari repository-mu. Lalu ganti nama satu kolom di
schema.sql tanpa mengubah query.sql dan jalankan generate lagi — baca
errornya. Itu kelas bug yang biasanya baru ketahuan di produksi.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.