SQLC: SQL tipado a partir de queries em Go
Como o SQLC gera código Go fortemente tipado a partir de queries SQL puras, com a interface Querier e sem reflexão em tempo de execução.
Publicada em · 9 min de leitura
Ao projetar a camada de acesso a dados em Go, os desenvolvedores costumam ser forçados a escolher entre dois extremos problemáticos:
- Usar um ORM tradicional (como GORM ou Hibernate): Esconde o SQL atrás de abstrações opacas, depende de reflexão dinâmica pesada em tempo de execução, dificulta a depuração de planos de execução (
EXPLAIN ANALYZE) e torna quase inevitável a introdução acidental do temido problema de consultas excessivas N+1. - Escrever Go puro (
database/sqloupgxmanual): Garante controle absoluto sobre as queries, mas exige centenas de linhas de código repetitivo derows.Scan(&a, &b, &c...). Além disso, qualquer erro de digitação no nome de uma coluna ou tipo incompatível só é descoberto em produção quando a query falha.
Para resolver esse dilema e unir o melhor dos dois mundos, surgiu o SQLC (sqlc.dev), uma das ferramentas mais aclamadas e adotadas pela comunidade moderna de Go.
A premissa do SQLC é simples e revolucionária: Você escreve o SQL puro; o SQLC gera código Go 100% tipado, seguro em tempo de compilação e com zero reflexão.
Esta aula disseca o funcionamento do compilador do SQLC, as anotações semânticas de queries, a configuração moderna com pgx/v5 e como a interface Querier gerada automaticamente simplifica suítes de testes unitários.
O Mapa da Aula
| Parte | Seção | O que você vai dominar |
|---|---|---|
| 1. Filosofia | 1 e 2 | O dilema ORM vs Go Puro e por que o SQLC compila SQL com segurança estática |
| 2. O Compilador por Dentro | 3 e 4 | O parser AST nativo do PostgreSQL em tempo de desenvolvimento e a geração de código |
| 3. Anotações de Queries | 5 e 6 | As diretivas essenciais: :one, :many, :exec e :execresult |
| 4. Configuração Moderna | 7 e 8 | Configurando sqlc.yaml para pgx/v5, tags JSON automáticas e a interface Querier |
| 5. Nulidade e Tipos | 9 | Tratamento de campos nulos com pgtype e sobrescrita de tipos (Type Overrides) |
| 6. Código de Produção | 10 e 11 | Exemplo completo de ponta a ponta, mocking trivial em testes e checklist de qualidade |
Pré-requisitos recomendados: database-sql e Connection Pooling, PostgreSQL com pgx e Mocks e Dublês de Teste.
Parte 1: O Dilema da Persistência em Go
1. Por que a comunidade Go rejeita ORMs tradicionais?
Em ecossistemas como Java (Hibernate) ou Ruby (ActiveRecord), os ORMs tentam mapear tabelas de banco diretamente para classes orientadas a objetos. No Go, esse paradigma entra em conflito direto com a filosofia de simplicidade e clareza da linguagem:
- Reflexão Pesada: ORMs dependem intensamente do pacote
reflectpara inspecionar campos de structs em tempo de execução, consumindo CPU desnecessária e alocando memória no Heap. - Complexidade Oculta: Escrever joins complexos, queries analíticas ou CTEs (Common Table Expressions) em DSLs de ORM costuma gerar códigos incompreensíveis. O desenvolvedor passa mais tempo aprendendo a sintaxe proprietária do ORM do que usando SQL.
- A Armadilha do N+1: ORMs com carregamento preguiçoso (lazy loading) mascaram queries adicionais dentro de laços de repetição, degradando a performance silenciosamente.
2. A Filosofia do SQLC
O SQLC inverte essa lógica através de uma abordagem orientada a SQL (SQL-First):
- Você continua sendo o mestre absoluto do seu banco de dados: você cria as tabelas com
CREATE TABLEe escreve suas consultas em arquivos.sqlpuros. - O SQLC lê suas queries, valida a sintaxe diretamente contra o motor do banco e gera structs Go correspondentes e métodos de repositório otimizados.
- O código Go gerado utiliza chamadas diretas de
QueryRoweScancompiladas estaticamente, com zero overhead de reflexão e desempenho idêntico a código escrito manualmente pelos engenheiros mais experientes.
Parte 2: Como o SQLC Opera Sob o Capô
3. O Parser Nativo do PostgreSQL em Tempo de Desenvolvimento
O maior diferencial do SQLC em relação a geradores de código ingênuos é que ele não usa expressões regulares para tentar adivinhar seu SQL:
O compilador do SQLC embute o próprio código-fonte do parser léxico e analisador sintático (AST) oficial do PostgreSQL:
- Ele carrega seus arquivos de schema (
schema.sql) e monta a árvore de catálogo de tabelas, colunas, tipos e restrições. - Ao ler sua query (
query.sql), ele valida se as colunas selecionadas realmente existem, se as tabelas foram declaradas e se os parâmetros$1,$2são compatíveis com os tipos das colunas no banco. - Se você errar uma vírgula ou tentar passar uma
stringpara um campoINTEGER, o comandosqlc generatefalha imediatamente no terminal com o número exato da linha do erro, impedindo que código quebrado chegue a ser compilado em Go!
Parte 3: Anotações de Queries no SQLC
Nos seus arquivos .sql, você instrui o SQLC sobre como cada consulta deve ser transformada em função Go através de um comentário estruturado logo acima da query:
-- name: <NomeDaFuncao> <ModificadorDeRetorno>
Existem 4 modificadores fundamentais:
| Modificador | O que faz no Go | Assinatura Gerada | Uso Típico |
|---|---|---|---|
:one | Retorna exatamente 1 registro ou erro | (T, error) | Busca por ID, e-mail único ou INSERT ... RETURNING |
:many | Retorna um slice de registros | ([]T, error) | Listagens com WHERE, paginação e filtros |
:exec | Executa a query e descarta o retorno | error | UPDATE, DELETE ou INSERT simples sem retorno |
:execresult | Retorna metadados de execução | (pgconn.CommandTag, error) | Quando você precisa checar RowsAffected() |
Parte 4: Configuração Moderna com sqlc.yaml
A partir da versão 2, o SQLC é configurado através de um arquivo sqlc.yaml na raiz do projeto.
Para projetos modernos de alto desempenho utilizando PostgreSQL com pgx, configuramos o backend nativo pgx/v5:
version: "2"
sql:
- engine: "postgresql"
schema: "sql/schema.sql" # Arquivo ou pasta com DDLs (CREATE TABLE)
queries: "sql/queries.sql" # Arquivo ou pasta com queries DML
gen:
go:
package: "db" # Nome do pacote Go gerado
out: "internal/db" # Pasta de destino do código Go
sql_package: "pgx/v5" # Gera código nativo para pgx v5!
emit_json_tags: true # Adiciona tags `json:"nome"` nas structs
emit_interface: true # Gera a interface Querier (crucial para testes!)
emit_empty_slices: true # Retorna []T vazio em vez de nil quando não há linhas
4. O Superpoder do emit_interface: true
Ao ativar a opção emit_interface: true, o SQLC gera automaticamente um arquivo querier.go contendo uma interface pública com todos os métodos das suas queries:
// Código gerado automaticamente pelo sqlc:
type Querier interface {
CriarUsuario(ctx context.Context, arg CriarUsuarioParams) (Usuario, error)
ObterUsuarioPorID(ctx context.Context, id int64) (Usuario, error)
ListarUsuariosAtivos(ctx context.Context) ([]Usuario, error)
AtualizarBio(ctx context.Context, arg AtualizarBioParams) error
DeletarUsuario(ctx context.Context, id int64) error
}
Isso significa que na sua camada de regras de negócio (Clean Architecture em Go), seu Use Case depende apenas da interface db.Querier. Em testes unitários, você pode mockar o banco de dados em segundos usando Mocks e Dublês de Teste sem precisar de contêineres Docker nem dependências externas!
Parte 5: Tratamento de Nulidade e Mapeamento de Tipos
O SQLC mapeia os tipos com precisão cirúrgica baseado nas restrições de nulidade do schema:
CREATE TABLE usuarios (
id BIGSERIAL PRIMARY KEY, -- int64
nome VARCHAR(100) NOT NULL, -- string pura
bio TEXT, -- pgtype.Text (anulável)
idade INTEGER, -- pgtype.Int4 (anulável)
created_at TIMESTAMP NOT NULL -- pgtype.Timestamp
);
- Campos
NOT NULL: Mapeiam diretamente para os tipos primitivos correspondentes do Go (string,int64,bool). - Campos Anuláveis (Sem
NOT NULL): Mapeiam para structs que suportam valores nulos sem causar pânico de ponteiro, como os tipos nativos do pgx (pgtype.Text{String: "...", Valid: true}).
Parte 6: Código Prático Completo de Produção
Vamos criar um módulo completo de gerenciamento de usuários demonstrando o fluxo: DDL $\to$ Queries $\to$ Geração $\to$ Consumo na Aplicação.
1. O Schema DDL (sql/schema.sql):
CREATE TABLE usuarios (
id BIGSERIAL PRIMARY KEY,
nome VARCHAR(100) NOT NULL,
email VARCHAR(150) UNIQUE NOT NULL,
bio TEXT,
ativo BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMP NOT NULL DEFAULT now()
);
2. As Consultas SQL (sql/queries.sql):
-- name: ObterUsuarioPorID :one
SELECT id, nome, email, bio, ativo, created_at
FROM usuarios
WHERE id = $1 LIMIT 1;
-- name: ListarUsuariosAtivos :many
SELECT id, nome, email, bio, ativo, created_at
FROM usuarios
WHERE ativo = true
ORDER BY nome ASC;
-- name: CriarUsuario :one
INSERT INTO usuarios (nome, email, bio)
VALUES ($1, $2, $3)
RETURNING id, nome, email, bio, ativo, created_at;
-- name: AtualizarBio :exec
UPDATE usuarios
SET bio = $2
WHERE id = $1;
-- name: DeletarUsuario :exec
DELETE FROM usuarios
WHERE id = $1;
3. Gerando o Código Go:
No terminal:
sqlc generate
O SQLC gera instantaneamente a pasta internal/db/ contendo:
models.go: StructUsuariocom todos os tipos tipados e tags JSON.querier.go: InterfaceQuerier.query.sql.go: Todos os métodos executáveis implementando queries nativas compgx/v5.
4. Consumindo no Código de Aplicação (main.go):
package main
import (
"context"
"fmt"
"log"
"github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxpool"
"meu-projeto/internal/db"
)
func main() {
ctx := context.Background()
// 1. Inicializa o pool de conexões do pgx
pool, err := pgxpool.New(ctx, "postgres://usuario:senha@localhost:5432/meubanco")
if err != nil {
log.Fatalf("falha ao conectar no postgres: %v", err)
}
defer pool.Close()
// 2. Instancia o conjunto de queries gerado pelo sqlc
queries := db.New(pool)
// 3. Execução type-safe: Criar Usuário
novoUser, err := queries.CriarUsuario(ctx, db.CriarUsuarioParams{
Nome: "Camila Rocha",
Email: "camila@empresa.com",
Bio: pgtype.Text{String: "Engenheira de Confiabilidade", Valid: true},
})
if err != nil {
log.Fatalf("falha ao criar usuário: %v", err)
}
fmt.Printf("Usuário criado com sucesso! ID: %d, Criado em: %v\n", novoUser.ID, novoUser.CreatedAt)
// 4. Execução type-safe: Listar Usuários
usuarios, err := queries.ListarUsuariosAtivos(ctx)
if err != nil {
log.Fatalf("falha ao listar: %v", err)
}
for _, u := range usuarios {
fmt.Printf("• %s (%s) - Ativo? %v\n", u.Nome, u.Email, u.Ativo)
}
}
Parte 7: Mockando o Banco em Testes Unitários com Querier
Como ativamos emit_interface: true, criar testes unitários para a camada de serviço se torna trivial. Você não precisa conectar em nenhum banco Docker:
package servico_test
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"meu-projeto/internal/db"
)
// MockQuerier implementa a interface db.Querier gerada pelo sqlc
type MockQuerier struct {
mock.Mock
db.Querier
}
func (m *MockQuerier) ObterUsuarioPorID(ctx context.Context, id int64) (db.Usuario, error) {
args := m.Called(ctx, id)
return args.Get(0).(db.Usuario), args.Error(1)
}
func TestBuscarUsuario(t *testing.T) {
mockDB := new(MockQuerier)
usuarioEsperado := db.Usuario{
ID: 42,
Nome: "Camila Rocha",
Email: "camila@empresa.com",
Ativo: true,
}
// Programa o mock
mockDB.On("ObterUsuarioPorID", mock.Anything, int64(42)).Return(usuarioEsperado, nil).Once()
// Executa a lógica de serviço
u, err := mockDB.ObterUsuarioPorID(context.Background(), 42)
assert.NoError(t, err)
assert.Equal(t, "Camila Rocha", u.Nome)
mockDB.AssertExpectations(t)
}
Parte 8: Checklist de Boas Práticas com SQLC
- Sempre use
sql_package: "pgx/v5": Aproveite o protocolo binário e o pool nativo de alta vazão sem o overhead dodatabase/sql. - Ative sempre
emit_interface: true: Permite injetar e mockar a interfacedb.Querierem testes unitários puros com extrema rapidez. - Mantenha
schema.sqle migrações sincronizados: O SQLC depende do schema DDL para saber quais colunas existem. Mantenha os arquivos DDL versionados no Git junto com suas migrações. - Use nomes de queries expressivos: Prefira nomes de domínio claros como
-- name: BuscarClientePorCPF :oneem vez de nomes genéricos como-- name: Select :one. - Integre com o comando
go generate: Adicione//go:generate sqlc generateno topo do seumain.gopara regenerar queries facilmente comgo generate ./....
Exercício Prático Proposto
- Escreva um schema para um sistema de e-commerce com tabelas
produtosecategorias. - Crie queries usando SQLC:
- Uma query com
-- name: ListarProdutosPorCategoria :manycom paginação (LIMIT $1 OFFSET $2). - Uma query de atualização com
-- name: DecrementarEstoque :execresult.
- Uma query com
- Gere o código com
sqlc generate. - Inspecione a struct gerada em
models.goe verifique os tipos e tags JSON automáticas.