PostgreSQL é um dos bancos de dados relacionais mais poderosos e populares. Embora Zig não tenha ainda um driver nativo escrito 100% em Zig, sua interoperabilidade excepcional com C permite usar a libpq — a biblioteca C oficial do PostgreSQL — de forma segura e elegante.
Neste guia, você aprenderá:
- Conectar ao PostgreSQL usando libpq
- Executar queries síncronas e assíncronas
- Prepared statements tipados
- Gerenciar transações
- Criar um wrapper moderno em Zig
Por que Zig + PostgreSQL?
| Aspecto | Go (pgx) | Rust (tokio-postgres) | C (libpq) | Zig + libpq |
|---|---|---|---|---|
| Segurança | Moderada | ✅ Alta | ❌ Baixa | ✅ Alta |
| Performance | Boa | ✅ Excelente | ✅ Excelente | ✅ Excelente |
| Complexidade | Baixa | Alta | Moderada | Moderada |
| Binário | Grande | Médio | Mínimo | Mínimo |
| Async | ✅ Sim | ✅ Sim | ⚠️ Manual | ✅ Sim |
Configuração do Ambiente
Instalando Dependências
# Ubuntu/Debian
sudo apt-get install postgresql libpq-dev
# macOS
brew install postgresql libpq
# Arch
sudo pacman -S postgresql-libs
Estrutura do Projeto
postgres-zig/
├── build.zig
├── src/
│ ├── main.zig
│ └── pg.zig # Nosso wrapper PostgreSQL
└── init.sql # Schema inicial
build.zig
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "postgres-zig",
.root_source_file = .{ .cwd_relative = "src/main.zig" },
.target = target,
.optimize = optimize,
});
// Link com libpq
exe.linkLibC();
exe.linkSystemLibrary("pq");
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
}
Conectando ao PostgreSQL
Usando libpq Diretamente
// src/main.zig
const std = @import("std");
const c = @cImport({
@cInclude("libpq-fe.h");
});
pub fn main() !void {
// String de conexão
const conninfo = "host=localhost port=5432 dbname=testdb user=postgres password=secret";
// Conecta ao banco
const conn = c.PQconnectdb(conninfo);
defer c.PQfinish(conn);
// Verifica status
if (c.PQstatus(conn) != c.CONNECTION_OK) {
std.debug.print("Falha na conexão: {s}\n", .{c.PQerrorMessage(conn)});
return error.ConnectionFailed;
}
std.debug.print("✅ Conectado ao PostgreSQL!\n");
}
Criando um Wrapper Tipado
// src/pg.zig - Wrapper moderno para libpq
const std = @import("std");
const c = @cImport({
@cInclude("libpq-fe.h");
});
pub const Connection = struct {
pg_conn: ?*c.PGconn,
allocator: std.mem.Allocator,
pub const ConnectError = error{
ConnectionFailed,
InvalidParams,
};
pub fn connect(allocator: std.mem.Allocator, conninfo: []const u8) ConnectError!Connection {
const c_conninfo = allocator.dupeZ(u8, conninfo) catch return error.InvalidParams;
defer allocator.free(c_conninfo);
const pg_conn = c.PQconnectdb(c_conninfo.ptr);
if (c.PQstatus(pg_conn) != c.CONNECTION_OK) {
c.PQfinish(pg_conn);
return error.ConnectionFailed;
}
return .{
.pg_conn = pg_conn,
.allocator = allocator,
};
}
pub fn close(self: *Connection) void {
if (self.pg_conn) |conn| {
c.PQfinish(conn);
self.pg_conn = null;
}
}
pub fn query(self: Connection, sql: []const u8) !Result {
const c_sql = try self.allocator.dupeZ(u8, sql);
defer self.allocator.free(c_sql);
const res = c.PQexec(self.pg_conn, c_sql.ptr);
const status = c.PQresultStatus(res);
if (status != c.PGRES_TUPLES_OK and status != c.PGRES_COMMAND_OK) {
const err = c.PQresultErrorMessage(res);
std.debug.print("Query error: {s}\n", .{err});
c.PQclear(res);
return error.QueryFailed;
}
return Result{
.pg_res = res,
.allocator = self.allocator,
};
}
pub fn exec(self: Connection, sql: []const u8) !u64 {
const result = try self.query(sql);
defer result.deinit();
return result.affectedRows();
}
};
pub const Result = struct {
pg_res: ?*c.PGresult,
allocator: std.mem.Allocator,
pub fn deinit(self: Result) void {
if (self.pg_res) |res| {
c.PQclear(res);
}
}
pub fn rows(self: Result) i32 {
return c.PQntuples(self.pg_res);
}
pub fn cols(self: Result) i32 {
return c.PQnfields(self.pg_res);
}
pub fn get(self: Result, row: i32, col: i32) ?[]const u8 {
if (c.PQgetisnull(self.pg_res, row, col) == 1) {
return null;
}
const val = c.PQgetvalue(self.pg_res, row, col);
return std.mem.span(val);
}
pub fn affectedRows(self: Result) u64 {
const cmd = c.PQcmdTuples(self.pg_res);
const str = std.mem.span(cmd);
return std.fmt.parseInt(u64, str, 10) catch 0;
}
};
Uso do Wrapper
const pg = @import("pg.zig");
pub fn main() !void {
const allocator = std.heap.page_allocator;
var conn = try pg.Connection.connect(
allocator,
"host=localhost dbname=myapp user=postgres password=secret"
);
defer conn.close();
// Executa query
var result = try conn.query("SELECT version()");
defer result.deinit();
if (result.get(0, 0)) |version| {
std.debug.print("PostgreSQL: {s}\n", .{version});
}
}
Queries e Resultados
SELECT com Mapeamento de Structs
const User = struct {
id: i32,
name: []const u8,
email: []const u8,
created_at: i64, // timestamp as unix
};
fn fetchUsers(conn: pg.Connection, allocator: std.mem.Allocator) ![]User {
var result = try conn.query(
"SELECT id, name, email, EXTRACT(EPOCH FROM created_at)::bigint " ++
"FROM users ORDER BY created_at DESC LIMIT 10"
);
defer result.deinit();
const row_count = result.rows();
var users = try allocator.alloc(User, @intCast(row_count));
var i: i32 = 0;
while (i < row_count) : (i += 1) {
users[@intCast(i)] = .{
.id = std.fmt.parseInt(i32, result.get(i, 0).?, 10) catch 0,
.name = try allocator.dupe(u8, result.get(i, 1).?),
.email = try allocator.dupe(u8, result.get(i, 2).?),
.created_at = std.fmt.parseInt(i64, result.get(i, 3).?, 10) catch 0,
};
}
return users;
}
INSERT com Retorno
fn createUser(conn: pg.Connection, name: []const u8, email: []const u8) !i32 {
const sql = try std.fmt.allocPrint(
conn.allocator,
"INSERT INTO users (name, email) VALUES ('{s}', '{s}') RETURNING id",
.{ name, email }
);
defer conn.allocator.free(sql);
var result = try conn.query(sql);
defer result.deinit();
return std.fmt.parseInt(i32, result.get(0, 0).?, 10) catch 0;
}
Prepared Statements
pub const Statement = struct {
conn: *pg.Connection,
name: []const u8,
pub fn prepare(conn: *pg.Connection, name: []const u8, sql: []const u8) !Statement {
const c_sql = try conn.allocator.dupeZ(u8, sql);
defer conn.allocator.free(c_sql);
const c_name = try conn.allocator.dupeZ(u8, name);
defer conn.allocator.free(c_name);
const res = c.PQprepare(
conn.pg_conn,
c_name.ptr,
c_sql.ptr,
0, // número de parâmetros (descobrir automaticamente)
null
);
defer c.PQclear(res);
if (c.PQresultStatus(res) != c.PGRES_COMMAND_OK) {
return error.PrepareFailed;
}
return .{
.conn = conn,
.name = try conn.allocator.dupe(u8, name),
};
}
pub fn deinit(self: *Statement) void {
// DEALLOCATE statement
const sql = std.fmt.allocPrint(
self.conn.allocator,
"DEALLOCATE {s}",
.{self.name}
) catch return;
defer self.conn.allocator.free(sql);
_ = self.conn.exec(sql) catch {};
self.conn.allocator.free(self.name);
}
pub fn execute(self: Statement, params: []const ?[]const u8) !pg.Result {
const n_params = @as(c_int, @intCast(params.len));
// Converte para formato C
var param_values = try self.conn.allocator.alloc(?[*:0]const u8, params.len);
defer self.conn.allocator.free(param_values);
for (params, 0..) |param, i| {
param_values[i] = if (param) |p| p.ptr else null;
}
const c_name = try self.conn.allocator.dupeZ(u8, self.name);
defer self.conn.allocator.free(c_name);
const res = c.PQexecPrepared(
self.conn.pg_conn,
c_name.ptr,
n_params,
param_values.ptr,
null, // param_lengths
null, // param_formats
0 // result_format (text)
);
const status = c.PQresultStatus(res);
if (status != c.PGRES_TUPLES_OK and status != c.PGRES_COMMAND_OK) {
const err = c.PQresultErrorMessage(res);
std.debug.print("Execute error: {s}\n", .{err});
c.PQclear(res);
return error.ExecuteFailed;
}
return pg.Result{
.pg_res = res,
.allocator = self.conn.allocator,
};
}
};
Uso de Prepared Statements
// Prepara statement uma vez
var stmt = try pg.Statement.prepare(&conn, "get_user",
"SELECT id, name, email FROM users WHERE id = $1"
);
defer stmt.deinit();
// Executa múltiplas vezes
var i: i32 = 1;
while (i <= 10) : (i += 1) {
const id_str = try std.fmt.allocPrint(allocator, "{d}", .{i});
defer allocator.free(id_str);
var result = try stmt.execute(&.{id_str});
defer result.deinit();
if (result.rows() > 0) {
std.debug.print("User: {s}\n", .{result.get(0, 1).?});
}
}
Transações
pub const Transaction = struct {
conn: *pg.Connection,
active: bool,
pub fn begin(conn: *pg.Connection) !Transaction {
_ = try conn.exec("BEGIN");
return .{
.conn = conn,
.active = true,
};
}
pub fn commit(self: *Transaction) !void {
if (!self.active) return;
_ = try self.conn.exec("COMMIT");
self.active = false;
}
pub fn rollback(self: *Transaction) !void {
if (!self.active) return;
_ = try self.conn.exec("ROLLBACK");
self.active = false;
}
pub fn deinit(self: *Transaction) void {
if (self.active) {
_ = self.conn.exec("ROLLBACK") catch {};
self.active = false;
}
}
};
// Uso
{
var tx = try pg.Transaction.begin(&conn);
defer tx.deinit(); // Auto-rollback se não commitar
_ = try conn.exec("INSERT INTO accounts (user_id, balance) VALUES (1, 100)");
_ = try conn.exec("INSERT INTO accounts (user_id, balance) VALUES (2, 200)");
try tx.commit();
}
Pool de Conexões (Básico)
pub const Pool = struct {
connections: std.ArrayList(*pg.Connection),
available: std.ArrayList(usize),
allocator: std.mem.Allocator,
conninfo: []const u8,
pub fn init(allocator: std.mem.Allocator, conninfo: []const u8, size: usize) !Pool {
var connections = try std.ArrayList(*pg.Connection).initCapacity(allocator, size);
var available = try std.ArrayList(usize).initCapacity(allocator, size);
var i: usize = 0;
while (i < size) : (i += 1) {
const conn = try allocator.create(pg.Connection);
conn.* = try pg.Connection.connect(allocator, conninfo);
try connections.append(conn);
try available.append(i);
}
return .{
.connections = connections,
.available = available,
.allocator = allocator,
.conninfo = try allocator.dupe(u8, conninfo),
};
}
pub fn deinit(self: *Pool) void {
for (self.connections.items) |conn| {
conn.close();
self.allocator.destroy(conn);
}
self.connections.deinit();
self.available.deinit();
self.allocator.free(self.conninfo);
}
pub fn acquire(self: *Pool) !*pg.Connection {
if (self.available.popOrNull()) |index| {
return self.connections.items[index];
}
return error.NoConnectionsAvailable;
}
pub fn release(self: *Pool, conn: *pg.Connection) !void {
// Encontra índice
for (self.connections.items, 0..) |c, i| {
if (c == conn) {
try self.available.append(i);
return;
}
}
return error.InvalidConnection;
}
};
Migrations
const migrations = [_][]const u8{
\\CREATE TABLE IF NOT EXISTS schema_migrations (
\\ version INTEGER PRIMARY KEY,
\\ applied_at TIMESTAMP DEFAULT NOW()
\\)
,
\\CREATE TABLE IF NOT EXISTS users (
\\ id SERIAL PRIMARY KEY,
\\ name VARCHAR(255) NOT NULL,
\\ email VARCHAR(255) UNIQUE NOT NULL,
\\ created_at TIMESTAMP DEFAULT NOW()
\\)
,
\\CREATE TABLE IF NOT EXISTS posts (
\\ id SERIAL PRIMARY KEY,
\\ user_id INTEGER REFERENCES users(id),
\\ title VARCHAR(255) NOT NULL,
\\ content TEXT,
\\ created_at TIMESTAMP DEFAULT NOW()
\\)
};
fn runMigrations(conn: pg.Connection) !void {
// Garante tabela de migrations existe
_ = try conn.exec(migrations[0]);
var i: usize = 1;
while (i < migrations.len) : (i += 1) {
const check_sql = try std.fmt.allocPrint(
conn.allocator,
"SELECT 1 FROM schema_migrations WHERE version = {d}",
.{i}
);
defer conn.allocator.free(check_sql);
var result = conn.query(check_sql) catch continue;
const exists = result.rows() > 0;
result.deinit();
if (!exists) {
_ = try conn.exec(migrations[i]);
const insert_sql = try std.fmt.allocPrint(
conn.allocator,
"INSERT INTO schema_migrations (version) VALUES ({d})",
.{i}
);
defer conn.allocator.free(insert_sql);
_ = try conn.exec(insert_sql);
std.debug.print("Applied migration {d}\n", .{i});
}
}
}
Async com epoll/io_uring
const linux = std.os.linux;
pub async fn queryAsync(conn: *pg.Connection, sql: []const u8) !pg.Result {
const c_sql = try conn.allocator.dupeZ(u8, sql);
defer conn.allocator.free(c_sql);
// Inicia query não-bloqueante
if (c.PQsendQuery(conn.pg_conn, c_sql.ptr) == 0) {
return error.SendFailed;
}
const fd = c.PQsocket(conn.pg_conn);
// Espera com epoll
var epoll_fd = try linux.epoll_create1(0);
defer linux.close(epoll_fd);
var event = linux.epoll_event{
.events = linux.EPOLL.IN,
.data = .{ .fd = fd },
};
try linux.epoll_ctl(epoll_fd, linux.EPOLL.CTL_ADD, fd, &event);
var events: [1]linux.epoll_event = undefined;
while (true) {
// Consome input
while (c.PQconsumeInput(conn.pg_conn) == 0) {
const nev = linux.epoll_wait(epoll_fd, &events, 1, -1);
if (nev < 0) return error.EpollFailed;
}
if (c.PQisBusy(conn.pg_conn) == 0) {
// Query completa
const res = c.PQgetResult(conn.pg_conn);
if (res) |r| {
return pg.Result{
.pg_res = r,
.allocator = conn.allocator,
};
}
// Mais resultados vindo
continue;
}
}
}
Melhores Práticas
✅ Faça
// ✅ Sempre use defer para cleanup
defer result.deinit();
// ✅ Verifique erros de conexão
if (conn.pg_conn == null or c.PQstatus(conn.pg_conn) != c.CONNECTION_OK) {
return error.ConnectionFailed;
}
// ✅ Use prepared statements para queries repetidas
var stmt = try pg.Statement.prepare(&conn, "stmt_name", sql);
defer stmt.deinit();
// ✅ Escape strings para evitar SQL injection
const safe = try escapeLiteral(allocator, user_input);
defer allocator.free(safe);
❌ Evite
// ❌ Concatenação de SQL sem escaping
const sql = "SELECT * FROM users WHERE name = '" ++ user_input ++ "'";
// ❌ Não fechar resultados antes de reusar variável
var result = try conn.query("SELECT 1");
result = try conn.query("SELECT 2"); // Memory leak!
// ❌ Ignorar erros silenciosamente
_ = conn.query(sql); // Pode falhar!
Próximos Passos
- 🔗 Zig e WebSockets — Combine com PostgreSQL para apps real-time
- ⚡ SIMD em Zig — Acelere processamento de dados
- 🧠 Tratamento de Erros em Zig — Padrões avançados de erro
- 📦 Zig Package Registry — Publique seu driver
Recursos Adicionais
- PostgreSQL C API Reference
- libpq Source Code
- pgx (Go driver) — Referência de design
Está usando PostgreSQL com Zig em produção? Compartilhe sua experiência!