Skip to main content

sqlc — Type-Safe Go Code Generation from SQL

sqlc is a tool that automatically generates type-safe Go code from SQL queries. You get both the expressiveness of SQL and Go's type safety simultaneously.


Introduction to sqlc​

sqlc solves both ORM's "hiding SQL" problem and raw SQL's "no types" problem.

SQL query files (.sql)
↓ sqlc generate
Type-safe Go code (models.go, query.go)
↓
Use directly (compile-time type checking)

Installation​

# Install with Go tool
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest

# macOS (Homebrew)
brew install sqlc

Project Structure​

myapp/
├── sqlc.yaml # sqlc configuration file
├── db/
│ ├── migrations/ # migration files
│ │ └── 001_init.sql
│ ├── queries/ # SQL query files
│ │ └── users.sql
│ └── sqlc/ # generated code (do not edit)
│ ├── db.go
│ ├── models.go
│ └── users.sql.go
└── main.go

sqlc.yaml Configuration File​

version: "2"
sql:
- engine: "postgresql"
queries: "./db/queries"
schema: "./db/migrations"
gen:
go:
package: "sqlcdb"
out: "./db/sqlc"
emit_json_tags: true
emit_prepared_queries: false
emit_interface: true
emit_exact_table_names: false
emit_empty_slices: true

Schema and Query Writing​

Migration Files (Schema Definition)​

-- db/migrations/001_init.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
age INT,
bio TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE posts (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title TEXT NOT NULL,
content TEXT,
published BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

Query Files​

Queries specify their name and return type with special comments.

-- db/queries/users.sql

-- name: GetUser :one
SELECT * FROM users
WHERE id = $1 LIMIT 1;

-- name: GetUserByEmail :one
SELECT * FROM users
WHERE email = $1 LIMIT 1;

-- name: ListUsers :many
SELECT * FROM users
ORDER BY created_at DESC
LIMIT $1 OFFSET $2;

-- name: CountUsers :one
SELECT COUNT(*) FROM users;

-- name: CreateUser :one
INSERT INTO users (name, email, age, bio)
VALUES ($1, $2, $3, $4)
RETURNING *;

-- name: UpdateUser :one
UPDATE users
SET name = $2,
age = $3,
bio = $4,
updated_at = NOW()
WHERE id = $1
RETURNING *;

-- name: DeleteUser :exec
DELETE FROM users
WHERE id = $1;

-- name: SearchUsers :many
SELECT * FROM users
WHERE name ILIKE $1 OR email ILIKE $1
ORDER BY name;

Query Return Type Specifiers​

SpecifierMeaning
:one*Model single row
:many[]Model multiple rows
:execerror no rows
:execresultsql.Result, error
:execrowsint64, error

Code Generation​

sqlc generate

Generated files:

models.go — auto-generated table structs

// Code generated by sqlc. DO NOT EDIT.
package sqlcdb

import (
"time"
)

type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Age sql.NullInt32 `json:"age"`
Bio sql.NullString `json:"bio"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

type Post struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
Title string `json:"title"`
Content sql.NullString `json:"content"`
Published bool `json:"published"`
CreatedAt time.Time `json:"created_at"`
}

users.sql.go — auto-generated query functions

// Code generated by sqlc. DO NOT EDIT.
package sqlcdb

const createUser = `-- name: CreateUser :one
INSERT INTO users (name, email, age, bio)
VALUES ($1, $2, $3, $4)
RETURNING id, name, email, age, bio, created_at, updated_at
`

type CreateUserParams struct {
Name string `json:"name"`
Email string `json:"email"`
Age sql.NullInt32 `json:"age"`
Bio sql.NullString `json:"bio"`
}

func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
row := q.db.QueryRowContext(ctx, createUser, arg.Name, arg.Email, arg.Age, arg.Bio)
var i User
err := row.Scan(
&i.ID, &i.Name, &i.Email, &i.Age, &i.Bio, &i.CreatedAt, &i.UpdatedAt,
)
return i, err
}

Practical Usage Example​

package main

import (
"context"
"database/sql"
"fmt"
"log"

_ "github.com/lib/pq"
"myapp/db/sqlc"
)

func main() {
db, err := sql.Open("postgres",
"host=localhost user=myuser password=mypass dbname=mydb sslmode=disable")
if err != nil {
log.Fatal(err)
}
defer db.Close()

queries := sqlcdb.New(db)
ctx := context.Background()

// Create user
user, err := queries.CreateUser(ctx, sqlcdb.CreateUserParams{
Name: "Kim Golang",
Email: "golang@example.com",
Age: sql.NullInt32{Int32: 30, Valid: true},
Bio: sql.NullString{String: "Go developer", Valid: true},
})
if err != nil {
log.Fatal("create user failed:", err)
}
fmt.Printf("Created: ID=%d, Name=%s\n", user.ID, user.Name)

// Query user
found, err := queries.GetUser(ctx, user.ID)
if err == sql.ErrNoRows {
fmt.Println("User not found")
} else if err != nil {
log.Fatal(err)
} else {
fmt.Printf("Found: %s <%s>\n", found.Name, found.Email)
}

// List users (pagination)
users, err := queries.ListUsers(ctx, sqlcdb.ListUsersParams{
Limit: 10,
Offset: 0,
})
if err != nil {
log.Fatal(err)
}
for _, u := range users {
fmt.Printf(" - [%d] %s\n", u.ID, u.Name)
}

// Update
updated, err := queries.UpdateUser(ctx, sqlcdb.UpdateUserParams{
ID: user.ID,
Name: "Kim Golang (updated)",
Age: sql.NullInt32{Int32: 31, Valid: true},
Bio: sql.NullString{String: "Senior Go developer", Valid: true},
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Updated: %s\n", updated.Name)

// Delete
if err := queries.DeleteUser(ctx, user.ID); err != nil {
log.Fatal(err)
}
fmt.Println("Deleted")
}

Using with Transactions​

sqlc can accept both *sql.DB and *sql.Tx.

func transferOwnership(ctx context.Context, db *sql.DB, fromUserID, postID, toUserID int64) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()

// Use sqlc in transaction
qtx := sqlcdb.New(tx)

// Verify post ownership
post, err := qtx.GetPost(ctx, postID)
if err != nil {
return fmt.Errorf("query post failed: %w", err)
}
if post.UserID != fromUserID {
return fmt.Errorf("not owner")
}

// Transfer ownership
if err := qtx.UpdatePostOwner(ctx, sqlcdb.UpdatePostOwnerParams{
ID: postID,
UserID: toUserID,
}); err != nil {
return fmt.Errorf("transfer ownership failed: %w", err)
}

return tx.Commit()
}

sqlc vs GORM vs database/sql Comparison​

Criteriadatabase/sqlGORMsqlc
SQL ControlFullLimitedFull
Type SafetyManualORM-basedCompile-time
Code AmountLargeSmallMedium
Complex QueriesEasyDifficultEasy
PerformanceBestLowBest
Learning CurveMediumLowLow
MigrationSeparate toolAutoMigrateSeparate tool

Recommended Scenarios:

  • database/sql: Complex custom queries, maximum performance required
  • GORM: Fast development, simple CRUD-focused apps
  • sqlc: Know SQL well and want type safety (recommended)

Key Summary​

  • sqlc auto-generates SQL → Go code for type safety
  • Don't edit generated code; modify SQL files then regenerate
  • Transactions work identically with the sqlc.New(tx) pattern
  • The workflow of "schema change → modify SQL → sqlc generate → compiler catches missing updates" is very powerful.