PostgreSQL em Zig: Criando um Driver Nativo

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?

AspectoGo (pgx)Rust (tokio-postgres)C (libpq)Zig + libpq
SegurançaModerada✅ Alta❌ Baixa✅ Alta
PerformanceBoa✅ Excelente✅ Excelente✅ Excelente
ComplexidadeBaixaAltaModeradaModerada
BinárioGrandeMédioMínimoMí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

  1. 🔗 Zig e WebSockets — Combine com PostgreSQL para apps real-time
  2. SIMD em Zig — Acelere processamento de dados
  3. 🧠 Tratamento de Erros em Zig — Padrões avançados de erro
  4. 📦 Zig Package Registry — Publique seu driver

Recursos Adicionais


Está usando PostgreSQL com Zig em produção? Compartilhe sua experiência!

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.