SQLite é o banco de dados embutido mais utilizado do mundo. Presente em smartphones, navegadores, sistemas operacionais e aplicações desktop, ele oferece um banco relacional completo em um único arquivo, sem necessidade de servidor. Combinar SQLite com Zig resulta em aplicações extremamente rápidas, com binários pequenos e sem dependências externas de runtime.
Neste tutorial, você vai aprender a integrar SQLite em projetos Zig do zero: desde abrir um banco de dados até operações CRUD completas, prepared statements, transações e tratamento robusto de erros.
Pré-requisitos
- Zig instalado (versão 0.13+). Veja o guia de instalação
- Conhecimento básico de Zig. Consulte a introdução ao Zig
- Familiaridade com SQL básico (SELECT, INSERT, UPDATE, DELETE)
Por que SQLite com Zig?
A combinação de Zig e SQLite é poderosa por vários motivos:
- Interoperabilidade nativa com C: Zig importa a API C do SQLite diretamente, sem wrappers ou bindings manuais
- Sem overhead de runtime: Nenhum garbage collector ou runtime pesado entre seu código e o banco de dados
- Binário único: SQLite é compilado estaticamente junto com seu programa Zig
- Controle de memória: Use os allocators de Zig para gerenciar memória de forma previsível
- Cross-compilation: Compile seu app + SQLite para qualquer plataforma com um único comando
Configurando o Projeto
Estrutura do Projeto
Crie um novo projeto Zig com a seguinte estrutura:
meu-projeto-sqlite/
├── build.zig
├── build.zig.zon
├── src/
│ └── main.zig
└── libs/
└── sqlite3/
├── sqlite3.c
└── sqlite3.h
Obtendo o SQLite
Baixe o código-fonte amalgamation do SQLite:
mkdir -p libs/sqlite3
cd libs/sqlite3
curl -O https://www.sqlite.org/2024/sqlite-amalgamation-3450000.zip
unzip sqlite-amalgamation-3450000.zip
cp sqlite-amalgamation-3450000/sqlite3.c .
cp sqlite-amalgamation-3450000/sqlite3.h .
rm -rf sqlite-amalgamation-3450000 sqlite-amalgamation-3450000.zip
Configurando o build.zig
Configure o build system de Zig para compilar o SQLite junto com seu projeto:
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "meu-app-sqlite",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
// Compilar SQLite como biblioteca C
exe.addCSourceFile(.{
.file = b.path("libs/sqlite3/sqlite3.c"),
.flags = &.{
"-DSQLITE_THREADSAFE=1",
"-DSQLITE_ENABLE_FTS5",
"-DSQLITE_ENABLE_JSON1",
},
});
exe.addIncludePath(b.path("libs/sqlite3"));
exe.linkLibC();
b.installArtifact(exe);
// Comando para executar
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
const run_step = b.step("run", "Executar a aplicação");
run_step.dependOn(&run_cmd.step);
}
Importando e Abrindo o Banco de Dados
O primeiro passo é importar a API C do SQLite e abrir (ou criar) um banco de dados:
const std = @import("std");
const c = @cImport({
@cInclude("sqlite3.h");
});
const SqliteError = error{
OpenFailed,
PrepareFailed,
StepFailed,
BindFailed,
ExecFailed,
TransactionFailed,
};
pub const Database = struct {
db: ?*c.sqlite3,
/// Abre ou cria um banco de dados SQLite.
/// O arquivo é criado automaticamente se não existir.
pub fn open(path: [*:0]const u8) !Database {
var db: ?*c.sqlite3 = null;
const result = c.sqlite3_open(path, &db);
if (result != c.SQLITE_OK) {
if (db) |d| {
std.log.err("Erro ao abrir banco: {s}", .{c.sqlite3_errmsg(d)});
_ = c.sqlite3_close(d);
}
return SqliteError.OpenFailed;
}
return Database{ .db = db };
}
/// Fecha a conexão com o banco de dados.
pub fn close(self: *Database) void {
if (self.db) |db| {
_ = c.sqlite3_close(db);
self.db = null;
}
}
};
Exemplo de Uso Básico
pub fn main() !void {
// Abrir banco de dados (cria o arquivo se não existir)
var db = try Database.open("meu_banco.db");
defer db.close();
std.debug.print("Banco de dados aberto com sucesso!\n", .{});
// Para usar banco de dados em memória (testes):
// var db_mem = try Database.open(":memory:");
// defer db_mem.close();
}
Executando SQL Direto
Para comandos SQL que não retornam dados (CREATE TABLE, INSERT simples, etc.), use sqlite3_exec:
pub const Database = struct {
db: ?*c.sqlite3,
// ... (open e close definidos acima)
/// Executa uma query SQL sem retorno de dados.
/// Ideal para CREATE TABLE, INSERT, UPDATE, DELETE simples.
pub fn exec(self: *Database, sql: [*:0]const u8) !void {
var err_msg: ?[*:0]u8 = null;
const result = c.sqlite3_exec(
self.db,
sql,
null,
null,
&err_msg,
);
if (result != c.SQLITE_OK) {
if (err_msg) |msg| {
std.log.err("Erro SQL: {s}", .{msg});
c.sqlite3_free(msg);
}
return SqliteError.ExecFailed;
}
}
};
Criando Tabelas
Vamos criar uma tabela de usuários para demonstrar todas as operações CRUD:
fn criarTabelas(db: *Database) !void {
try db.exec(
\\CREATE TABLE IF NOT EXISTS usuarios (
\\ id INTEGER PRIMARY KEY AUTOINCREMENT,
\\ nome TEXT NOT NULL,
\\ email TEXT NOT NULL UNIQUE,
\\ idade INTEGER,
\\ ativo BOOLEAN DEFAULT 1,
\\ criado_em DATETIME DEFAULT CURRENT_TIMESTAMP
\\);
);
try db.exec(
\\CREATE TABLE IF NOT EXISTS posts (
\\ id INTEGER PRIMARY KEY AUTOINCREMENT,
\\ usuario_id INTEGER NOT NULL,
\\ titulo TEXT NOT NULL,
\\ conteudo TEXT,
\\ publicado_em DATETIME DEFAULT CURRENT_TIMESTAMP,
\\ FOREIGN KEY (usuario_id) REFERENCES usuarios(id)
\\);
);
try db.exec(
\\CREATE INDEX IF NOT EXISTS idx_posts_usuario
\\ON posts(usuario_id);
);
std.debug.print("Tabelas criadas com sucesso!\n", .{});
}
Prepared Statements
Prepared statements são a forma segura e eficiente de executar queries com parâmetros. Eles previnem SQL injection e permitem reutilização de queries compiladas.
pub const Statement = struct {
stmt: ?*c.sqlite3_stmt,
/// Prepara uma query SQL para execução.
pub fn prepare(db: *Database, sql: [*:0]const u8) !Statement {
var stmt: ?*c.sqlite3_stmt = null;
const result = c.sqlite3_prepare_v2(
db.db,
sql,
-1,
&stmt,
null,
);
if (result != c.SQLITE_OK) {
if (db.db) |d| {
std.log.err("Erro ao preparar: {s}", .{c.sqlite3_errmsg(d)});
}
return SqliteError.PrepareFailed;
}
return Statement{ .stmt = stmt };
}
/// Vincula um inteiro a um parâmetro da query (1-indexed).
pub fn bindInt(self: *Statement, index: c_int, value: c_int) !void {
const result = c.sqlite3_bind_int(self.stmt, index, value);
if (result != c.SQLITE_OK) return SqliteError.BindFailed;
}
/// Vincula um inteiro de 64 bits a um parâmetro da query.
pub fn bindInt64(self: *Statement, index: c_int, value: i64) !void {
const result = c.sqlite3_bind_int64(self.stmt, index, value);
if (result != c.SQLITE_OK) return SqliteError.BindFailed;
}
/// Vincula um texto a um parâmetro da query.
pub fn bindText(self: *Statement, index: c_int, text: [*:0]const u8) !void {
const result = c.sqlite3_bind_text(
self.stmt,
index,
text,
-1,
c.SQLITE_TRANSIENT,
);
if (result != c.SQLITE_OK) return SqliteError.BindFailed;
}
/// Executa um passo da query. Retorna true se há uma linha de resultado.
pub fn step(self: *Statement) !bool {
const result = c.sqlite3_step(self.stmt);
if (result == c.SQLITE_ROW) return true;
if (result == c.SQLITE_DONE) return false;
return SqliteError.StepFailed;
}
/// Obtém uma coluna como inteiro.
pub fn columnInt(self: *Statement, index: c_int) c_int {
return c.sqlite3_column_int(self.stmt, index);
}
/// Obtém uma coluna como inteiro de 64 bits.
pub fn columnInt64(self: *Statement, index: c_int) i64 {
return c.sqlite3_column_int64(self.stmt, index);
}
/// Obtém uma coluna como texto.
pub fn columnText(self: *Statement, index: c_int) ?[*:0]const u8 {
return @ptrCast(c.sqlite3_column_text(self.stmt, index));
}
/// Reseta o statement para reutilização.
pub fn reset(self: *Statement) void {
_ = c.sqlite3_reset(self.stmt);
_ = c.sqlite3_clear_bindings(self.stmt);
}
/// Libera os recursos do statement.
pub fn finalize(self: *Statement) void {
if (self.stmt) |stmt| {
_ = c.sqlite3_finalize(stmt);
self.stmt = null;
}
}
};
Operações CRUD Completas
CREATE: Inserindo Dados
const Usuario = struct {
id: i64,
nome: []const u8,
email: []const u8,
idade: c_int,
ativo: bool,
};
fn inserirUsuario(db: *Database, nome: [*:0]const u8, email: [*:0]const u8, idade: c_int) !i64 {
var stmt = try Statement.prepare(db,
\\INSERT INTO usuarios (nome, email, idade)
\\VALUES (?1, ?2, ?3)
);
defer stmt.finalize();
try stmt.bindText(1, nome);
try stmt.bindText(2, email);
try stmt.bindInt(3, idade);
_ = try stmt.step();
// Retorna o ID gerado
const id = c.sqlite3_last_insert_rowid(db.db);
std.debug.print("Usuário inserido com ID: {d}\n", .{id});
return id;
}
fn inserirVariosUsuarios(db: *Database) !void {
const usuarios = [_]struct { nome: [*:0]const u8, email: [*:0]const u8, idade: c_int }{
.{ .nome = "Ana Silva", .email = "ana@exemplo.com", .idade = 28 },
.{ .nome = "Carlos Souza", .email = "carlos@exemplo.com", .idade = 35 },
.{ .nome = "Maria Oliveira", .email = "maria@exemplo.com", .idade = 22 },
.{ .nome = "João Santos", .email = "joao@exemplo.com", .idade = 41 },
.{ .nome = "Beatriz Lima", .email = "bia@exemplo.com", .idade = 30 },
};
// Usar prepared statement reutilizável para múltiplas inserções
var stmt = try Statement.prepare(db,
\\INSERT INTO usuarios (nome, email, idade)
\\VALUES (?1, ?2, ?3)
);
defer stmt.finalize();
for (usuarios) |u| {
try stmt.bindText(1, u.nome);
try stmt.bindText(2, u.email);
try stmt.bindInt(3, u.idade);
_ = try stmt.step();
stmt.reset();
}
std.debug.print("Inseridos {d} usuários com sucesso.\n", .{usuarios.len});
}
READ: Consultando Dados
fn buscarUsuarioPorId(db: *Database, id: c_int) !void {
var stmt = try Statement.prepare(db,
\\SELECT id, nome, email, idade, ativo
\\FROM usuarios
\\WHERE id = ?1
);
defer stmt.finalize();
try stmt.bindInt(1, id);
if (try stmt.step()) {
const nome = stmt.columnText(1) orelse "N/A";
const email = stmt.columnText(2) orelse "N/A";
const idade = stmt.columnInt(3);
const ativo = stmt.columnInt(4);
std.debug.print(
\\Usuário encontrado:
\\ ID: {d}
\\ Nome: {s}
\\ Email: {s}
\\ Idade: {d}
\\ Ativo: {s}
\\
, .{
stmt.columnInt(0),
nome,
email,
idade,
if (ativo == 1) "Sim" else "Não",
});
} else {
std.debug.print("Usuário com ID {d} não encontrado.\n", .{id});
}
}
fn listarTodosUsuarios(db: *Database) !void {
var stmt = try Statement.prepare(db,
\\SELECT id, nome, email, idade, ativo
\\FROM usuarios
\\ORDER BY nome ASC
);
defer stmt.finalize();
std.debug.print("\n--- Lista de Usuários ---\n", .{});
std.debug.print("{s:<5} {s:<20} {s:<25} {s:<6} {s:<6}\n", .{
"ID", "Nome", "Email", "Idade", "Ativo",
});
std.debug.print("{s}\n", .{"-" ** 65});
var count: u32 = 0;
while (try stmt.step()) {
const id = stmt.columnInt(0);
const nome = stmt.columnText(1) orelse "N/A";
const email = stmt.columnText(2) orelse "N/A";
const idade = stmt.columnInt(3);
const ativo = stmt.columnInt(4);
std.debug.print("{d:<5} {s:<20} {s:<25} {d:<6} {s:<6}\n", .{
id,
nome,
email,
idade,
if (ativo == 1) "Sim" else "Não",
});
count += 1;
}
std.debug.print("\nTotal: {d} usuários\n", .{count});
}
fn buscarUsuariosPorIdade(db: *Database, idade_min: c_int, idade_max: c_int) !void {
var stmt = try Statement.prepare(db,
\\SELECT nome, email, idade
\\FROM usuarios
\\WHERE idade BETWEEN ?1 AND ?2
\\ORDER BY idade ASC
);
defer stmt.finalize();
try stmt.bindInt(1, idade_min);
try stmt.bindInt(2, idade_max);
std.debug.print("\nUsuários com idade entre {d} e {d}:\n", .{ idade_min, idade_max });
while (try stmt.step()) {
const nome = stmt.columnText(0) orelse "N/A";
const email = stmt.columnText(1) orelse "N/A";
const idade = stmt.columnInt(2);
std.debug.print(" {s} ({s}) - {d} anos\n", .{ nome, email, idade });
}
}
UPDATE: Atualizando Dados
fn atualizarEmail(db: *Database, id: c_int, novo_email: [*:0]const u8) !void {
var stmt = try Statement.prepare(db,
\\UPDATE usuarios
\\SET email = ?1
\\WHERE id = ?2
);
defer stmt.finalize();
try stmt.bindText(1, novo_email);
try stmt.bindInt(2, id);
_ = try stmt.step();
const alterados = c.sqlite3_changes(db.db);
if (alterados > 0) {
std.debug.print("Email do usuário {d} atualizado para: {s}\n", .{ id, novo_email });
} else {
std.debug.print("Nenhum usuário encontrado com ID {d}.\n", .{id});
}
}
fn desativarUsuario(db: *Database, id: c_int) !void {
var stmt = try Statement.prepare(db,
\\UPDATE usuarios
\\SET ativo = 0
\\WHERE id = ?1
);
defer stmt.finalize();
try stmt.bindInt(1, id);
_ = try stmt.step();
const alterados = c.sqlite3_changes(db.db);
std.debug.print("Usuários desativados: {d}\n", .{alterados});
}
DELETE: Removendo Dados
fn deletarUsuario(db: *Database, id: c_int) !void {
var stmt = try Statement.prepare(db,
\\DELETE FROM usuarios
\\WHERE id = ?1
);
defer stmt.finalize();
try stmt.bindInt(1, id);
_ = try stmt.step();
const deletados = c.sqlite3_changes(db.db);
if (deletados > 0) {
std.debug.print("Usuário {d} deletado com sucesso.\n", .{id});
} else {
std.debug.print("Nenhum usuário encontrado com ID {d}.\n", .{id});
}
}
fn deletarUsuariosInativos(db: *Database) !void {
try db.exec(
\\DELETE FROM usuarios WHERE ativo = 0
);
const deletados = c.sqlite3_changes(db.db);
std.debug.print("Removidos {d} usuários inativos.\n", .{deletados});
}
Transações
Transações garantem que múltiplas operações sejam executadas atomicamente. Se qualquer operação falhar, todas são revertidas:
pub const Transaction = struct {
db: *Database,
/// Inicia uma transação.
pub fn begin(db: *Database) !Transaction {
try db.exec("BEGIN TRANSACTION");
return Transaction{ .db = db };
}
/// Confirma todas as operações da transação.
pub fn commit(self: *Transaction) !void {
try self.db.exec("COMMIT");
}
/// Reverte todas as operações da transação.
pub fn rollback(self: *Transaction) void {
self.db.exec("ROLLBACK") catch |err| {
std.log.err("Erro ao fazer rollback: {}", .{err});
};
}
};
fn transferirDados(db: *Database) !void {
var tx = try Transaction.begin(db);
errdefer tx.rollback();
// Múltiplas operações dentro da transação
try db.exec(
\\INSERT INTO usuarios (nome, email, idade) VALUES ('Teste 1', 'teste1@ex.com', 25)
);
try db.exec(
\\INSERT INTO usuarios (nome, email, idade) VALUES ('Teste 2', 'teste2@ex.com', 30)
);
try db.exec(
\\INSERT INTO posts (usuario_id, titulo, conteudo)
\\VALUES (1, 'Meu primeiro post', 'Conteúdo do post...')
);
// Se tudo deu certo, confirma
try tx.commit();
std.debug.print("Transação concluída com sucesso!\n", .{});
}
Note o uso de errdefer para garantir o rollback automático em caso de erro. Esse é um padrão idiomático de Zig para gerenciamento de recursos. Veja mais sobre esse padrão em errdefer em Zig.
Inserções em Lote com Performance
Para inserir grandes volumes de dados, combine transações com prepared statements reutilizáveis:
fn inserirEmLote(db: *Database, allocator: std.mem.Allocator) !void {
_ = allocator;
var tx = try Transaction.begin(db);
errdefer tx.rollback();
var stmt = try Statement.prepare(db,
\\INSERT INTO usuarios (nome, email, idade)
\\VALUES (?1, ?2, ?3)
);
defer stmt.finalize();
// Simulando inserção de 1000 registros
var i: u32 = 0;
while (i < 1000) : (i += 1) {
// Em um caso real, os dados viriam de outra fonte
try stmt.bindText(1, "Usuário Lote");
try stmt.bindText(2, "lote@exemplo.com");
try stmt.bindInt(3, @intCast(i % 60 + 18));
_ = try stmt.step();
stmt.reset();
}
try tx.commit();
std.debug.print("Inseridos 1000 registros em lote.\n", .{});
}
Essa abordagem pode ser 100x mais rápida do que inserções individuais sem transação, pois o SQLite não precisa fazer fsync no disco para cada operação.
Queries com JOIN
Consultas que combinam dados de múltiplas tabelas:
fn listarPostsComAutor(db: *Database) !void {
var stmt = try Statement.prepare(db,
\\SELECT
\\ p.id,
\\ p.titulo,
\\ u.nome AS autor,
\\ p.publicado_em
\\FROM posts p
\\INNER JOIN usuarios u ON u.id = p.usuario_id
\\ORDER BY p.publicado_em DESC
);
defer stmt.finalize();
std.debug.print("\n--- Posts Recentes ---\n", .{});
while (try stmt.step()) {
const id = stmt.columnInt(0);
const titulo = stmt.columnText(1) orelse "Sem título";
const autor = stmt.columnText(2) orelse "Desconhecido";
const data = stmt.columnText(3) orelse "N/A";
std.debug.print("[{d}] \"{s}\" por {s} em {s}\n", .{
id, titulo, autor, data,
});
}
}
fn contarPostsPorUsuario(db: *Database) !void {
var stmt = try Statement.prepare(db,
\\SELECT u.nome, COUNT(p.id) AS total_posts
\\FROM usuarios u
\\LEFT JOIN posts p ON p.usuario_id = u.id
\\GROUP BY u.id
\\ORDER BY total_posts DESC
);
defer stmt.finalize();
std.debug.print("\n--- Posts por Usuário ---\n", .{});
while (try stmt.step()) {
const nome = stmt.columnText(0) orelse "N/A";
const total = stmt.columnInt(1);
std.debug.print(" {s}: {d} posts\n", .{ nome, total });
}
}
Tratamento Robusto de Erros
Um wrapper de erro mais detalhado que captura informações do SQLite:
pub const SqliteDetailedError = struct {
code: c_int,
message: []const u8,
pub fn format(
self: SqliteDetailedError,
comptime fmt: []const u8,
options: std.fmt.FormatOptions,
writer: anytype,
) !void {
_ = fmt;
_ = options;
try writer.print("SQLite Error {d}: {s}", .{ self.code, self.message });
}
};
fn getLastError(db: *Database) SqliteDetailedError {
const code = c.sqlite3_errcode(db.db);
const msg = c.sqlite3_errmsg(db.db);
return .{
.code = code,
.message = std.mem.span(msg),
};
}
fn execComRetry(db: *Database, sql: [*:0]const u8, max_tentativas: u32) !void {
var tentativa: u32 = 0;
while (tentativa < max_tentativas) : (tentativa += 1) {
var err_msg: ?[*:0]u8 = null;
const result = c.sqlite3_exec(db.db, sql, null, null, &err_msg);
if (result == c.SQLITE_OK) return;
if (err_msg) |msg| {
std.log.warn(
"Tentativa {d}/{d} falhou: {s}",
.{ tentativa + 1, max_tentativas, msg },
);
c.sqlite3_free(msg);
}
// Se o banco está travado, espera e tenta novamente
if (result == c.SQLITE_BUSY or result == c.SQLITE_LOCKED) {
std.time.sleep(100 * std.time.ns_per_ms);
continue;
}
return SqliteError.ExecFailed;
}
return SqliteError.ExecFailed;
}
Configuração de Performance
Otimize o SQLite para diferentes cenários de uso:
fn configurarPerformance(db: *Database) !void {
// WAL mode: permite leituras concorrentes durante escritas
try db.exec("PRAGMA journal_mode=WAL");
// Sincronização: NORMAL é um bom equilíbrio entre segurança e velocidade
try db.exec("PRAGMA synchronous=NORMAL");
// Cache em memória: 64MB (em páginas de 4KB)
try db.exec("PRAGMA cache_size=-65536");
// Tamanho da página: 4KB (padrão otimizado)
try db.exec("PRAGMA page_size=4096");
// Habilitar chaves estrangeiras
try db.exec("PRAGMA foreign_keys=ON");
// Armazenar temporários em memória
try db.exec("PRAGMA temp_store=MEMORY");
// Timeout de busy (5 segundos)
_ = c.sqlite3_busy_timeout(db.db, 5000);
std.debug.print("Configurações de performance aplicadas.\n", .{});
}
Exemplo Completo: Aplicação CRUD
Reunindo tudo em uma aplicação funcional:
const std = @import("std");
const c = @cImport({
@cInclude("sqlite3.h");
});
// Inclua aqui as definições de Database, Statement e Transaction
// mostradas nas seções anteriores
pub fn main() !void {
// 1. Abrir banco de dados
var db = try Database.open("app.db");
defer db.close();
// 2. Configurar performance
try configurarPerformance(&db);
// 3. Criar tabelas
try criarTabelas(&db);
// 4. Inserir dados
const id_ana = try inserirUsuario(&db, "Ana Silva", "ana@exemplo.com", 28);
_ = try inserirUsuario(&db, "Carlos Souza", "carlos@exemplo.com", 35);
_ = try inserirUsuario(&db, "Maria Oliveira", "maria@exemplo.com", 22);
// 5. Consultar dados
try buscarUsuarioPorId(&db, @intCast(id_ana));
try listarTodosUsuarios(&db);
try buscarUsuariosPorIdade(&db, 25, 35);
// 6. Atualizar dados
try atualizarEmail(&db, 1, "ana.silva@novodominio.com");
// 7. Transação
try transferirDados(&db);
// 8. Listar posts com autores
try listarPostsComAutor(&db);
try contarPostsPorUsuario(&db);
// 9. Deletar dados
try desativarUsuario(&db, 3);
try deletarUsuariosInativos(&db);
// 10. Listar resultado final
try listarTodosUsuarios(&db);
std.debug.print("\nAplicação CRUD concluída com sucesso!\n", .{});
}
Boas Práticas
Ao trabalhar com SQLite em Zig, siga estas recomendações:
Sempre use prepared statements para queries com parâmetros. Nunca concatene strings SQL diretamente – isso evita SQL injection e melhora a performance.
Use
deferpara liberar recursos: Sempre chamestmt.finalize()edb.close()comdeferpara garantir a liberação mesmo em caso de erro.Transações para múltiplas escritas: Envolva inserções e atualizações em lote em uma transação. A diferença de performance é dramática.
errdeferpara rollback: Useerrdefer tx.rollback()para reverter transações automaticamente quando uma operação falhar.WAL mode em produção: O modo WAL (Write-Ahead Logging) melhora significativamente a concorrência de leitura/escrita.
Timeout de busy: Configure
sqlite3_busy_timeoutpara evitar erros de “database is locked” em ambientes concorrentes.Valide o retorno: Sempre verifique os códigos de retorno das funções SQLite. A API de Zig com error unions torna isso natural.
Dicas de Debug
Para debugar problemas com SQLite em Zig:
fn debugQuery(db: *Database, sql: [*:0]const u8) void {
// Mostrar o plano de execução da query
const explain_sql = std.fmt.allocPrintZ(
std.heap.page_allocator,
"EXPLAIN QUERY PLAN {s}",
.{sql},
) catch return;
defer std.heap.page_allocator.free(explain_sql);
var stmt = Statement.prepare(db, explain_sql) catch return;
defer stmt.finalize();
std.debug.print("\nPlano de execução:\n", .{});
while (stmt.step() catch false) {
const detail = stmt.columnText(3) orelse "N/A";
std.debug.print(" {s}\n", .{detail});
}
}
Alternativas para Bancos de Dados em Zig
Se o SQLite não atende às suas necessidades, considere:
- PostgreSQL com Zig: Para aplicações que precisam de um banco de dados cliente-servidor robusto com suporte a JSON, arrays e tipos customizados.
- Redis com Zig: Para cache em memória, filas de mensagens e pub/sub de alta performance.
- Arquivos mapeados em memória: Para dados simples, Zig oferece excelente suporte a mmap via a stdlib.
Próximos Passos
Agora que você domina SQLite com Zig, explore estes tópicos relacionados:
- PostgreSQL com Zig: Conecte-se a bancos de dados PostgreSQL para aplicações distribuídas
- Redis com Zig: Implemente cache e pub/sub de alta performance
- Servidor HTTP em Zig: Combine SQLite com uma API HTTP para criar serviços web completos
- Tratamento de erros: Aprofunde-se nos error unions e errdefer
- Gerenciamento de memória: Entenda allocators para otimizar o uso de memória com bancos de dados
- Testes em Zig: Escreva testes automatizados para suas operações de banco de dados
- Cross-compilation: Compile sua aplicação SQLite para qualquer plataforma com um comando
SQLite com Zig é uma combinação poderosa para aplicações que precisam de um banco de dados confiável sem a complexidade de um servidor dedicado. Com o código deste tutorial como base, você pode construir desde ferramentas CLI até servidores web completos com persistência de dados.