zcatsql/REFERENCE.md
reugenio c5e6cec4a6 refactor: rename zsqlite to zcatsql
Consistent naming with zcat ecosystem (zcatui, zcatgui, zcatsql).
All lowercase per Zig naming conventions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-09 02:19:52 +01:00

39 KiB

zcatsql - Manual de Referencia Técnica

Versión: 1.0.0 Zig: 0.15.2 SQLite: 3.47.2 Fecha: 2025-12-08

Tabla de Contenidos

  1. Introducción
  2. Instalación y Uso
  3. Estructura del Proyecto
  4. API Principal
  5. Gestión de Errores
  6. Transacciones
  7. Funciones de Usuario
  8. Hooks y Callbacks
  9. Extensiones
  10. Características Avanzadas
  11. Audit Log
  12. Tipos y Constantes
  13. Ejemplos de Uso

Introducción

zcatsql es un wrapper idiomático de SQLite para Zig que compila SQLite amalgamation directamente en el binario, resultando en un ejecutable único sin dependencias externas.

Características principales:

  • Zero dependencias runtime
  • API idiomática Zig (errores, allocators, iteradores)
  • Binario único y portable
  • Compatible con bases de datos SQLite existentes
  • Soporte completo para FTS5, JSON1, R-Tree
  • Sistema de auditoría con cadena de hash

Instalación y Uso

Como dependencia en build.zig

const zcatsql = b.dependency("zcatsql", .{
    .target = target,
    .optimize = optimize,
});
exe.root_module.addImport("zcatsql", zcatsql.module("zcatsql"));

Uso básico

const sqlite = @import("zcatsql");

pub fn main() !void {
    var db = try sqlite.open("test.db");
    defer db.close();

    try db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)");

    var stmt = try db.prepare("INSERT INTO users (name) VALUES (?)");
    defer stmt.finalize();

    try stmt.bindText(1, "Alice");
    _ = try stmt.step();
}

Estructura del Proyecto

src/
├── root.zig          # Exports públicos principales
├── c.zig             # Bindings C de SQLite
├── errors.zig        # Gestión de errores
├── types.zig         # Tipos comunes (OpenFlags, ColumnType, etc.)
├── database.zig      # Conexión a base de datos
├── statement.zig     # Prepared statements y Row
├── functions.zig     # Funciones de usuario y hooks
├── backup.zig        # Backup y Blob I/O
├── pool.zig          # Connection pooling
├── serialize.zig     # Serialización/deserialización
├── session.zig       # Change tracking y changesets
├── vtable.zig        # Virtual tables
├── fts5.zig          # Full-text search
├── json.zig          # Funciones JSON
├── rtree.zig         # Índices espaciales
└── audit/
    ├── mod.zig       # Exports del módulo audit
    ├── entry.zig     # Estructura Entry
    ├── context.zig   # Contexto de auditoría
    ├── log.zig       # Sistema principal de audit
    ├── index.zig     # Gestión de índice
    ├── writer.zig    # Escritura de logs
    └── verify.zig    # Verificación de integridad

API Principal

Database

Archivo: src/database.zig Re-exportado en: root.zig

Apertura y Cierre

Función Descripción
open(path: [:0]const u8) !Database Abre base de datos en path
openWithFlags(path, flags: OpenFlags) !Database Abre con flags específicos
openUri(uri: [:0]const u8) !Database Abre usando URI
openUriAlloc(allocator, uri) !Database Abre URI (versión allocator)
close() void Cierra la conexión
filename(db_name: [:0]const u8) ?[]const u8 Obtiene path del archivo
// Ejemplos
var db = try sqlite.open("mydb.sqlite");
defer db.close();

var db2 = try sqlite.openWithFlags("readonly.db", .{ .read_only = true });

var db3 = try sqlite.openUri("file:memdb?mode=memory&cache=shared");

Ejecución de Consultas

Función Descripción
exec(sql: [:0]const u8) !void Ejecuta SQL directamente
execAlloc(allocator, sql) !void Ejecuta SQL (versión allocator)
prepare(sql: [:0]const u8) !Statement Crea prepared statement
prepareAlloc(allocator, sql) !Statement Prepara (versión allocator)
try db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)");

var stmt = try db.prepare("SELECT * FROM users WHERE id = ?");
defer stmt.finalize();

Transacciones

Función Descripción
begin() !void Inicia transacción (DEFERRED)
beginImmediate() !void Inicia transacción IMMEDIATE
beginExclusive() !void Inicia transacción EXCLUSIVE
commit() !void Confirma transacción
rollback() !void Revierte transacción
savepoint(allocator, name) !void Crea savepoint
release(allocator, name) !void Libera savepoint
rollbackTo(allocator, name) !void Revierte a savepoint
try db.begin();
errdefer db.rollback() catch {};

try db.exec("INSERT INTO users (name) VALUES ('Alice')");
try db.exec("INSERT INTO users (name) VALUES ('Bob')");

try db.commit();

Información de la Base de Datos

Función Descripción
lastInsertRowId() i64 Último rowid insertado
changes() i32 Cambios en transacción actual
totalChanges() i32 Total de cambios desde apertura
errorMessage() []const u8 Último mensaje de error
errorCode() c_int Código de error
extendedErrorCode() c_int Código de error extendido
isReadOnly(db_name) bool Verifica si es solo lectura

Configuración (PRAGMA)

Función Descripción
setForeignKeys(enabled: bool) !void Habilita/deshabilita foreign keys
setJournalMode(allocator, mode) !void Configura modo de journal
setSynchronous(allocator, mode) !void Configura modo de sincronización
enableWalMode(allocator) !void Habilita modo WAL
setAutoVacuum(allocator, mode) !void Configura auto-vacuum
setCacheSize(allocator, size) !void Tamaño de caché
setCaseSensitiveLike(enabled) !void LIKE case-sensitive
setDeferForeignKeys(enabled) !void Diferir verificación FK
setLockingMode(allocator, mode) !void Modo de bloqueo
setQueryOnly(enabled) !void Solo lectura
setRecursiveTriggers(enabled) !void Triggers recursivos
setSecureDelete(enabled) !void Borrado seguro
setPageSize(allocator, size) !void Tamaño de página
setMaxPageCount(allocator, count) !void Máximo de páginas
setTempStore(allocator, mode) !void Almacenamiento temporal
setWalAutoCheckpoint(allocator, pages) !void Checkpoint automático WAL
try db.setForeignKeys(true);
try db.enableWalMode(allocator);
try db.setCacheSize(allocator, 10000);

Mantenimiento

Función Descripción
vacuum() !void Compacta la base de datos
incrementalVacuum(allocator, pages) !void Vacuum incremental
integrityCheck(allocator) ![]const u8 Verifica integridad
quickCheck(allocator) ![]const u8 Verificación rápida
optimize() !void Optimiza planificador
walCheckpoint(allocator, mode) !void Checkpoint WAL

Bases de Datos Adjuntas

Función Descripción
attach(allocator, path, schema) !void Adjunta base de datos
attachMemory(allocator, schema) !void Adjunta DB en memoria
detach(allocator, schema) !void Desadjunta base de datos
listDatabases(allocator) ![]DatabaseInfo Lista DBs adjuntas
try db.attach(allocator, "/path/to/other.db", "other");
// Ahora puedes usar: SELECT * FROM other.table_name
try db.detach(allocator, "other");

Límites

Función Descripción
getLimit(limit_type: Limit) i32 Obtiene valor de límite
setLimit(limit_type, value) i32 Establece límite
const max_sql = db.getLimit(.sql_length);
_ = db.setLimit(.sql_length, 500000);

Control de Archivos

Función Descripción
setFileControlInt(db_name, op, value) !void Control de archivo
getPersistWal(db_name) !bool Obtiene persistencia WAL
setPersistWal(db_name, persist) !void Establece persistencia WAL
setChunkSize(db_name, size) !void Tamaño de chunk
getDataVersion(db_name) !u32 Versión de datos

Statement

Archivo: src/statement.zig Re-exportado en: root.zig

Ciclo de Vida

Función Descripción
finalize() void Finaliza statement
reset() !void Resetea para re-ejecución
clearBindings() !void Limpia todos los bindings

Metadatos

Función Descripción
sql() []const u8 Texto SQL original
expandedSql(allocator) ![]u8 SQL con parámetros expandidos
isReadOnly() bool Verifica si es solo lectura
parameterCount() i32 Número de parámetros
parameterIndex(name) ?i32 Índice de parámetro nombrado
parameterName(index) ?[]const u8 Nombre de parámetro
columnCount() i32 Número de columnas
columnName(index) []const u8 Nombre de columna
columnType(index) ColumnType Tipo de columna
columnDeclType(index) ?[]const u8 Tipo declarado
columnDatabaseName(index) ?[]const u8 Base de datos origen
columnTableName(index) ?[]const u8 Tabla origen
columnOriginName(index) ?[]const u8 Columna origen

Binding de Parámetros (índice 1-based)

Función Descripción
bindNull(index) !void Vincula NULL
bindInt(index, value: i64) !void Vincula entero
bindFloat(index, value: f64) !void Vincula flotante
bindText(index, value: []const u8) !void Vincula texto
bindBlob(index, value: []const u8) !void Vincula blob
bindBool(index, value: bool) !void Vincula booleano
bindZeroblob(index, size: i32) !void Vincula zeroblob
bindTimestamp(index, ts: i64) !void Vincula timestamp ISO 8601
bindCurrentTime(index) !void Vincula tiempo actual
var stmt = try db.prepare("INSERT INTO users (name, age, active) VALUES (?, ?, ?)");
defer stmt.finalize();

try stmt.bindText(1, "Alice");
try stmt.bindInt(2, 30);
try stmt.bindBool(3, true);
_ = try stmt.step();

Binding con Nombres

Función Descripción
bindNullNamed(name) !void Vincula NULL por nombre
bindIntNamed(name, value) !void Vincula entero por nombre
bindFloatNamed(name, value) !void Vincula flotante por nombre
bindTextNamed(name, value) !void Vincula texto por nombre
bindBlobNamed(name, value) !void Vincula blob por nombre
bindBoolNamed(name, value) !void Vincula booleano por nombre
bindTimestampNamed(name, ts) !void Vincula timestamp por nombre
bindCurrentTimeNamed(name) !void Vincula tiempo actual por nombre
var stmt = try db.prepare("INSERT INTO users (name, age) VALUES (:name, :age)");
try stmt.bindTextNamed(":name", "Bob");
try stmt.bindIntNamed(":age", 25);

Binding por Lotes

Función Descripción
bindAll(values: anytype) !void Vincula tupla de valores
bindValue(index, value) !void Vincula valor tipado
rebind(values: anytype) !void Reset + bind
var stmt = try db.prepare("INSERT INTO users (name, age) VALUES (?, ?)");
try stmt.bindAll(.{ "Charlie", @as(i64, 35) });
_ = try stmt.step();

try stmt.rebind(.{ "Diana", @as(i64, 28) });
_ = try stmt.step();

Ejecución

Función Descripción
step() !bool Ejecuta un paso (retorna: ¿hay fila?)
while (try stmt.step()) {
    const name = stmt.columnText(0);
    std.debug.print("Name: {s}\n", .{name});
}

Lectura de Columnas

Función Descripción
columnInt(index) i64 Lee como i64
columnFloat(index) f64 Lee como f64
columnText(index) []const u8 Lee como texto
columnBlob(index) []const u8 Lee como blob
columnBool(index) bool Lee como bool
columnBytes(index) i32 Obtiene tamaño en bytes
columnIsNull(index) bool Verifica si es NULL

Iteradores

Función Descripción
iterator() RowIterator Obtiene iterador de filas
forEach(callback) !void Itera con callback
collectAll(allocator) ![]Row Colecta todas las filas
var stmt = try db.prepare("SELECT id, name FROM users");
defer stmt.finalize();

var iter = stmt.iterator();
while (iter.next()) |row| {
    const id = row.int(0);
    const name = row.text(1);
    std.debug.print("User {}: {s}\n", .{id, name});
}

Row y RowIterator

Archivo: src/statement.zig

RowIterator

Función Descripción
next() ?Row Obtiene siguiente fila o null

Row - Acceso a Datos

Función Descripción
int(index) i64 Lee entero
float(index) f64 Lee flotante
text(index) []const u8 Lee texto
blob(index) []const u8 Lee blob
boolean(index) bool Lee booleano
isNull(index) bool Verifica NULL
columnType(index) ColumnType Tipo de columna
columnName(index) []const u8 Nombre de columna
columnCount() i32 Número de columnas

Row - Mapeo a Estructuras

Función Descripción
to(comptime T) !T Mapea a struct (stack)
toAlloc(T, allocator) !T Mapea a struct (heap)
freeStruct(T, value, allocator) void Libera struct asignado
toValues(allocator) ![]Value Colecta todos los valores
const User = struct {
    id: i64,
    name: []const u8,
    email: ?[]const u8,
};

var stmt = try db.prepare("SELECT id, name, email FROM users");
defer stmt.finalize();

var iter = stmt.iterator();
while (iter.next()) |row| {
    const user = try row.to(User);
    std.debug.print("User: {s}\n", .{user.name});
}

Gestión de Errores

Archivo: src/errors.zig

Error Enum

pub const Error = error{
    SqliteError,        // Error genérico
    InternalError,      // Error interno de SQLite
    PermissionDenied,   // Permiso denegado
    Abort,              // Operación abortada
    Busy,               // Base de datos ocupada
    Locked,             // Tabla bloqueada
    OutOfMemory,        // Sin memoria
    ReadOnly,           // Base de datos solo lectura
    Interrupt,          // Operación interrumpida
    IoError,            // Error de I/O
    Corrupt,            // Base de datos corrupta
    NotFound,           // No encontrado
    Full,               // Disco lleno
    CantOpen,           // No se puede abrir
    Protocol,           // Error de protocolo
    Empty,              // Vacío
    Schema,             // Schema cambió
    TooBig,             // Demasiado grande
    Constraint,         // Violación de constraint
    Mismatch,           // Tipo no coincide
    Misuse,             // Uso incorrecto de API
    NoLfs,              // LFS no soportado
    Auth,               // Autorización denegada
    Format,             // Error de formato
    Range,              // Fuera de rango
    NotADatabase,       // No es una base de datos
    Notice,             // Notificación
    Warning,            // Advertencia
    Row,                // Hay fila disponible
    Done,               // Operación completada
};

Funciones de Error

Función Descripción
resultToError(result: c_int) ?Error Convierte código SQLite a Error
errorDescription(err: Error) []const u8 Obtiene descripción del error
db.exec("INVALID SQL") catch |err| {
    const desc = sqlite.errorDescription(err);
    std.debug.print("Error: {s}\n", .{desc});
    std.debug.print("SQLite message: {s}\n", .{db.errorMessage()});
};

Transacciones

Transacciones Básicas

// Transacción simple
try db.begin();
errdefer db.rollback() catch {};

try db.exec("INSERT INTO accounts (balance) VALUES (1000)");
try db.exec("UPDATE accounts SET balance = balance - 100 WHERE id = 1");

try db.commit();

Tipos de Transacción

// DEFERRED (por defecto) - bloquea al primer acceso
try db.begin();

// IMMEDIATE - bloquea inmediatamente para escritura
try db.beginImmediate();

// EXCLUSIVE - bloqueo exclusivo completo
try db.beginExclusive();

Savepoints

try db.begin();

try db.exec("INSERT INTO log (msg) VALUES ('step 1')");

try db.savepoint(allocator, "checkpoint1");

try db.exec("INSERT INTO log (msg) VALUES ('step 2')");

// Algo salió mal, revertir solo al savepoint
try db.rollbackTo(allocator, "checkpoint1");

// O liberar el savepoint si todo está bien
// try db.release(allocator, "checkpoint1");

try db.commit();

Funciones de Usuario

Archivo: src/functions.zig

Funciones Escalares

fn myUpperFn(ctx: *sqlite.FunctionContext, args: []const *sqlite.FunctionValue) void {
    if (args.len != 1) {
        ctx.setError("Expected 1 argument");
        return;
    }

    const text = args[0].asText() orelse {
        ctx.setNull();
        return;
    };

    // Convertir a mayúsculas (simplificado)
    var result: [256]u8 = undefined;
    const len = @min(text.len, result.len);
    for (text[0..len], 0..) |c, i| {
        result[i] = std.ascii.toUpper(c);
    }

    ctx.setText(result[0..len]);
}

// Registrar función
try db.createScalarFunction("my_upper", 1, myUpperFn);

// Usar en SQL
try db.exec("SELECT my_upper(name) FROM users");

// Eliminar función
try db.removeFunction("my_upper", 1);

Funciones de Agregación

const SumState = struct {
    total: i64 = 0,
};

fn sumStepFn(ctx: *sqlite.AggregateContext, args: []const *sqlite.FunctionValue) void {
    const state = ctx.getAggregateContext(SumState) orelse return;
    if (args[0].asInt()) |val| {
        state.total += val;
    }
}

fn sumFinalFn(ctx: *sqlite.AggregateContext) void {
    const state = ctx.getAggregateContext(SumState) orelse {
        ctx.setInt(0);
        return;
    };
    ctx.setInt(state.total);
}

try db.createAggregateFunction("my_sum", 1, sumStepFn, sumFinalFn);

Collations Personalizadas

fn caseInsensitiveCompare(a: []const u8, b: []const u8) i32 {
    // Comparación case-insensitive
    const len = @min(a.len, b.len);
    for (a[0..len], b[0..len]) |ca, cb| {
        const la = std.ascii.toLower(ca);
        const lb = std.ascii.toLower(cb);
        if (la < lb) return -1;
        if (la > lb) return 1;
    }
    if (a.len < b.len) return -1;
    if (a.len > b.len) return 1;
    return 0;
}

try db.createCollation("NOCASE_CUSTOM", caseInsensitiveCompare);

// Usar en SQL
try db.exec("SELECT * FROM users ORDER BY name COLLATE NOCASE_CUSTOM");

Hooks y Callbacks

Archivo: src/functions.zig y src/database.zig

Commit/Rollback Hooks

fn onCommit(user_data: ?*anyopaque) bool {
    std.debug.print("Transaction committed!\n", .{});
    return false; // false = permitir commit, true = rollback
}

fn onRollback(user_data: ?*anyopaque) void {
    std.debug.print("Transaction rolled back!\n", .{});
}

db.setCommitHook(onCommit);
db.setRollbackHook(onRollback);

Update Hook

fn onUpdate(
    user_data: ?*anyopaque,
    op: sqlite.UpdateOperation,
    db_name: []const u8,
    table_name: []const u8,
    rowid: i64,
) void {
    const op_str = switch (op) {
        .insert => "INSERT",
        .update => "UPDATE",
        .delete => "DELETE",
    };
    std.debug.print("{s} on {s}.{s} rowid={d}\n", .{op_str, db_name, table_name, rowid});
}

db.setUpdateHook(onUpdate);

Pre-Update Hook

fn onPreUpdate(ctx: *sqlite.PreUpdateContext) void {
    // Acceder a valores antes y después del cambio
    if (ctx.oldValue(0)) |old_val| {
        std.debug.print("Old value: {any}\n", .{old_val.asInt()});
    }
    if (ctx.newValue(0)) |new_val| {
        std.debug.print("New value: {any}\n", .{new_val.asInt()});
    }
}

db.setPreUpdateHook(onPreUpdate);

Authorizer

fn authorizer(
    user_data: ?*anyopaque,
    action: sqlite.AuthAction,
    arg1: ?[]const u8,
    arg2: ?[]const u8,
    db_name: ?[]const u8,
    trigger_name: ?[]const u8,
) sqlite.AuthResult {
    // Denegar DROP TABLE
    if (action == .drop_table) {
        return .deny;
    }
    return .ok;
}

db.setAuthorizer(authorizer);

Progress Handler

fn progressCallback(user_data: ?*anyopaque) bool {
    // Llamado cada N operaciones
    // Retornar true para interrumpir la operación
    return false;
}

// Llamar cada 1000 operaciones
db.setProgressHandler(1000, progressCallback);

Busy Handler

fn busyHandler(user_data: ?*anyopaque, count: i32) bool {
    // count = número de veces que se ha llamado
    if (count > 5) return false; // Rendirse después de 5 intentos

    std.time.sleep(100 * std.time.ns_per_ms); // Esperar 100ms
    return true; // Reintentar
}

db.setBusyHandler(busyHandler);

// O usar timeout simple
try db.setBusyTimeout(5000); // 5 segundos

Limpiar Hooks

db.clearHooks(); // Elimina todos los hooks

Extensiones

FTS5 - Búsqueda Full-Text

Archivo: src/fts5.zig

var fts = sqlite.Fts5.init(&db, allocator);

// Crear tabla FTS5
try fts.createSimpleTable("documents", &.{ "title", "content" });

// O con tokenizer específico
try fts.createTableWithTokenizer("docs", &.{ "title", "body" }, .porter);

// Insertar datos
try db.exec("INSERT INTO documents (title, content) VALUES ('Hello', 'World content')");

// Búsqueda simple
const results = try fts.search("documents", "world", &.{ "title", "content" }, 10);
defer allocator.free(results);

// Búsqueda con highlighting
const highlighted = try fts.searchWithHighlight(
    "documents",
    "world",
    1, // columna content
    "<b>",
    "</b>",
    10,
);

// Búsqueda con ranking BM25
const ranked = try fts.searchWithBM25("documents", "world", &.{ "title", "content" }, 10);

Tokenizers Disponibles

Tokenizer Descripción
.unicode61 Por defecto, soporta Unicode
.ascii Solo ASCII
.porter Stemming Porter (inglés)
.trigram N-gramas de 3 caracteres

JSON1 - Funciones JSON

Archivo: src/json.zig

var json_helper = sqlite.Json.init(&db, allocator);

// Validar JSON
const valid = try json_helper.isValid("{\"name\": \"Alice\"}");

// Extraer valores
const name = try json_helper.extractText("{\"user\": {\"name\": \"Bob\"}}", "$.user.name");
const age = try json_helper.extractInt("{\"age\": 30}", "$.age");

// Modificar JSON
const updated = try json_helper.set(
    "{\"name\": \"Alice\"}",
    "$.age",
    "30",
);

// Crear objetos
const obj = try json_helper.createObject(&.{
    .{ "name", "\"Charlie\"" },
    .{ "age", "25" },
});

// Crear arrays
const arr = try json_helper.createArray(&.{ "1", "2", "3" });

// Longitud de array
const len = try json_helper.arrayLength("[1, 2, 3, 4]", "$");

R-Tree - Índices Espaciales

Archivo: src/rtree.zig

var rtree = sqlite.RTree.init(&db, allocator);

// Crear tabla R-Tree 2D
try rtree.createSimpleTable2D("locations");

// Insertar puntos
try rtree.insertPoint2D("locations", 1, 40.7128, -74.0060); // NYC
try rtree.insertPoint2D("locations", 2, 34.0522, -118.2437); // LA

// Insertar bounding box
try rtree.insertBox2D("locations", 3, 40.0, 41.0, -75.0, -73.0);

// Búsqueda espacial
const bbox = sqlite.BoundingBox2D{
    .min_x = 39.0,
    .max_x = 42.0,
    .min_y = -76.0,
    .max_y = -72.0,
};

const ids = try rtree.getIntersectingIds2D("locations", bbox);
defer rtree.freeIds(ids);

for (ids) |id| {
    std.debug.print("Found location: {d}\n", .{id});
}

BoundingBox2D

// Crear desde centro
const box = sqlite.BoundingBox2D.fromCenter(0, 0, 10, 10);

// Crear desde punto
const point = sqlite.BoundingBox2D.fromPoint(5, 5);

// Operaciones
const intersects = box.intersects(other_box);
const contains = box.contains(other_box);
const contains_point = box.containsPoint(3, 3);
const area = box.area();
const center = box.center(); // returns {x, y}

GeoCoord

const nyc = sqlite.GeoCoord{ .lat = 40.7128, .lon = -74.0060 };
const la = sqlite.GeoCoord{ .lat = 34.0522, .lon = -118.2437 };

const distance_km = nyc.distanceKm(la); // Fórmula Haversine

Virtual Tables

Archivo: src/vtable.zig

// Definir estructura de fila
const FileRow = struct {
    name: []const u8,
    size: i64,
    modified: i64,
};

// Crear tabla virtual simple
const FilesVTable = sqlite.SimpleVTable(FileRow);

// Implementar métodos requeridos
const files_module = FilesVTable.Module{
    .create = createFilesTable,
    .destroy = destroyFilesTable,
    .open = openCursor,
    .close = closeCursor,
    .next = nextRow,
    .column = getColumn,
    .rowid = getRowId,
    .eof = isEof,
};

// Registrar módulo
try db.createModule("files", files_module);

// Usar en SQL
try db.exec("CREATE VIRTUAL TABLE my_files USING files('/path/to/dir')");
try db.exec("SELECT name, size FROM my_files WHERE size > 1000");

Características Avanzadas

Backup y Blob I/O

Archivo: src/backup.zig

Backup

// Backup simple a archivo
try sqlite.backupToFile(&source_db, "/path/to/backup.db");

// Backup con control detallado
var backup = try sqlite.Backup.initMain(&dest_db, &source_db);
defer backup.deinit();

while (true) {
    const done = try backup.step(100); // 100 páginas por paso
    if (done) break;

    const remaining = backup.remaining();
    const total = backup.pagecount();
    std.debug.print("Progress: {d}/{d}\n", .{total - remaining, total});
}

try backup.finish();

// Cargar desde archivo
var db = try sqlite.loadFromFile("/path/to/backup.db");

Blob I/O

// Abrir blob para lectura/escritura
var blob = try sqlite.Blob.open(
    &db,
    "main",
    "files",
    "content",
    rowid,
    true, // writable
);
defer blob.deinit();

// Leer
const size = blob.bytes();
var buffer: [1024]u8 = undefined;
try blob.read(&buffer, 0);

// Escribir
try blob.write("new data", 0);

// Leer todo
const all_data = try blob.readAll(allocator);
defer allocator.free(all_data);

Connection Pool

Archivo: src/pool.zig

// Crear pool con máximo 10 conexiones
var pool = try sqlite.ConnectionPool.init(allocator, "mydb.sqlite", 10);
defer pool.deinit();

// Adquirir conexión
var conn = try pool.acquire();
defer pool.release(conn);

// Usar conexión
try conn.exec("SELECT * FROM users");

// Estadísticas
const capacity = pool.capacity();
const open = pool.openCount();
const in_use = pool.inUseCount();

Serialización

Archivo: src/serialize.zig

// Serializar a bytes
const bytes = try sqlite.serialize.toBytes(&db, allocator, "main");
defer allocator.free(bytes);

// Guardar a archivo
try sqlite.serialize.saveToFile(&db, allocator, "/path/to/db.bin");

// Deserializar desde bytes
var db2 = try sqlite.serialize.fromBytes(bytes, ":memory:");
defer db2.close();

// Cargar desde archivo
var db3 = try sqlite.serialize.loadFromFile(allocator, "/path/to/db.bin");
defer db3.close();

// Clonar a memoria
var mem_db = try sqlite.serialize.cloneToMemory(&db, allocator);
defer mem_db.close();

// Comparar bases de datos
const equal = try sqlite.serialize.equals(&db1, &db2, allocator);

// Obtener tamaño serializado
const size = try sqlite.serialize.serializedSize(&db, "main");

Session y Changesets

Archivo: src/session.zig

// Crear sesión
var session = try sqlite.Session.init(&db, "main");
defer session.deinit();

// Rastrear tabla
try session.attach("users");

// Realizar cambios
try db.exec("INSERT INTO users (name) VALUES ('Alice')");
try db.exec("UPDATE users SET name = 'Bob' WHERE id = 1");

// Obtener changeset
const changeset = try session.changeset(allocator);
defer allocator.free(changeset);

// Invertir changeset
const inverse = try sqlite.invertChangeset(changeset, allocator);
defer allocator.free(inverse);

// Aplicar changeset a otra base de datos
try sqlite.applyChangeset(&other_db, changeset, conflictHandler, null);

// Concatenar changesets
const combined = try sqlite.concatChangesets(&.{ changeset1, changeset2 }, allocator);

Manejo de Conflictos

fn conflictHandler(
    user_data: ?*anyopaque,
    conflict_type: sqlite.ConflictType,
    iter: *sqlite.ChangesetIterator,
) sqlite.ConflictAction {
    return switch (conflict_type) {
        .data => .replace,     // Reemplazar datos existentes
        .not_found => .omit,   // Omitir si no existe
        .conflict => .abort,   // Abortar en conflicto
        .constraint => .abort, // Abortar en violación de constraint
        .foreign_key => .abort,
    };
}

Snapshots

// Crear snapshot (requiere WAL mode)
try db.enableWalMode(allocator);

const snapshot = try db.getSnapshot("main");
defer snapshot.deinit();

// Abrir snapshot en otra conexión
try other_db.openSnapshot("main", snapshot);

// Recuperar snapshot
try db.recoverSnapshot("main");

Audit Log

Archivo: src/audit/

El sistema de Audit Log proporciona registro completo de operaciones de base de datos con verificación de integridad mediante cadena de hash.

Configuración

const sqlite = @import("zcatsql");

var db = try sqlite.open("myapp.db");
defer db.close();

// Inicializar audit log
var audit = try sqlite.AuditLog.init(allocator, &db, .{
    .log_dir = "/var/log/myapp/audit",
    .app_name = "my_application",
    .user = "admin",
    .rotation = .{
        .max_bytes = 100 * 1024 * 1024, // 100MB
        .max_age_days = 30,
    },
    .capture = .{
        .sql = true,
        .before_values = true,
        .after_values = true,
        .exclude_tables = &.{ "sqlite_sequence", "_migrations" },
    },
});
defer audit.deinit();

// IMPORTANTE: Llamar start() después de que el struct esté en su ubicación final
audit.start();

Uso

// Cambiar usuario actual
try audit.setUser("john_doe");

// Cambiar nombre de aplicación
try audit.setApp("admin_panel");

// Las operaciones de base de datos se registran automáticamente
try db.exec("INSERT INTO users (name) VALUES ('Alice')");
try db.exec("UPDATE users SET name = 'Bob' WHERE id = 1");
try db.exec("DELETE FROM users WHERE id = 1");

// Forzar rotación de archivo
try audit.rotate();

// Flush a disco
try audit.flush();

// Obtener estadísticas
const stats = audit.stats();
std.debug.print("Entries: {d}, Bytes: {d}, Files: {d}\n", .{
    stats.total_entries,
    stats.total_bytes,
    stats.file_count,
});

Verificación de Integridad

// Verificar cadena de hash completa
var result = try sqlite.verifyAuditChain(allocator, "/var/log/myapp/audit");
defer result.deinit(allocator);

if (result.valid) {
    std.debug.print("Audit log verified: {d} entries in {d} files\n", .{
        result.entries_verified,
        result.files_verified,
    });
} else {
    std.debug.print("INTEGRITY ERROR at seq {?d}: {s}\n", .{
        result.first_invalid_seq,
        result.error_message orelse "unknown error",
    });
}

Formato de Log

Los logs se almacenan en formato JSON Lines (un objeto JSON por línea):

{"seq":1,"ts":1733667135000000,"tx_id":1,"ctx":{"user":"admin","app":"myapp","host":"server1","pid":12345},"op":"INSERT","table":"users","rowid":1,"prev_hash":"0000...","hash":"a1b2..."}
{"seq":2,"ts":1733667135000100,"tx_id":1,"ctx":{"user":"admin","app":"myapp","host":"server1","pid":12345},"op":"UPDATE","table":"users","rowid":1,"before":{"col0":1,"col1":"Alice"},"prev_hash":"a1b2...","hash":"c3d4..."}

Estructura del Directorio

/var/log/myapp/audit/
├── index.json                    # Metadatos de todos los archivos
├── 0001_20251208_143200.log     # Primer archivo de log
├── 0002_20251208_153000.log     # Segundo archivo (después de rotación)
└── ...

Componentes del Módulo Audit

Archivo Descripción
mod.zig Exports públicos
entry.zig Estructura Entry y serialización JSON
context.zig AuditContext (user, app, host, pid)
log.zig AuditLog principal con hooks SQLite
index.zig Gestión de index.json
writer.zig Escritura de archivos con rotación
verify.zig Verificación de cadena de hash

Tipos y Constantes

Archivo: src/types.zig

OpenFlags

pub const OpenFlags = struct {
    read_only: bool = false,
    read_write: bool = true,
    create: bool = true,
    uri: bool = false,
    memory: bool = false,
    no_mutex: bool = false,
    full_mutex: bool = false,
    shared_cache: bool = false,
    private_cache: bool = false,
    no_follow: bool = false,

    pub fn toInt(self: OpenFlags) c_int { ... }
};

ColumnType

pub const ColumnType = enum(c_int) {
    integer = 1,
    float = 2,
    text = 3,
    blob = 4,
    null_value = 5,

    pub fn fromInt(value: c_int) ?ColumnType { ... }
};

Limit

pub const Limit = enum(c_int) {
    length = 0,
    sql_length = 1,
    column = 2,
    expr_depth = 3,
    compound_select = 4,
    vdbe_op = 5,
    function_arg = 6,
    attached = 7,
    like_pattern_length = 8,
    variable_number = 9,
    trigger_depth = 10,
    worker_threads = 11,
};

UpdateOperation

pub const UpdateOperation = enum(c_int) {
    insert = 18,
    delete = 9,
    update = 23,

    pub fn fromInt(value: c_int) ?UpdateOperation { ... }
};

AuthAction

pub const AuthAction = enum(c_int) {
    create_index = 1,
    create_table = 2,
    create_temp_index = 3,
    create_temp_table = 4,
    create_temp_trigger = 5,
    create_temp_view = 6,
    create_trigger = 7,
    create_view = 8,
    delete = 9,
    drop_index = 10,
    drop_table = 11,
    drop_temp_index = 12,
    drop_temp_table = 13,
    drop_temp_trigger = 14,
    drop_temp_view = 15,
    drop_trigger = 16,
    drop_view = 17,
    insert = 18,
    pragma = 19,
    read = 20,
    select = 21,
    transaction = 22,
    update = 23,
    attach = 24,
    detach = 25,
    alter_table = 26,
    reindex = 27,
    analyze = 28,
    create_vtable = 29,
    drop_vtable = 30,
    function = 31,
    savepoint = 32,
    recursive = 33,

    pub fn fromInt(value: c_int) ?AuthAction { ... }
};

AuthResult

pub const AuthResult = enum(c_int) {
    ok = 0,
    deny = 1,
    ignore = 2,
};

Ejemplos de Uso

Ejemplo Básico Completo

const std = @import("std");
const sqlite = @import("zcatsql");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    // Abrir base de datos
    var db = try sqlite.open("example.db");
    defer db.close();

    // Configurar
    try db.setForeignKeys(true);
    try db.enableWalMode(allocator);

    // Crear tablas
    try db.exec(
        \\CREATE TABLE IF NOT EXISTS users (
        \\    id INTEGER PRIMARY KEY,
        \\    name TEXT NOT NULL,
        \\    email TEXT UNIQUE,
        \\    created_at TEXT DEFAULT CURRENT_TIMESTAMP
        \\)
    );

    try db.exec(
        \\CREATE TABLE IF NOT EXISTS posts (
        \\    id INTEGER PRIMARY KEY,
        \\    user_id INTEGER NOT NULL REFERENCES users(id),
        \\    title TEXT NOT NULL,
        \\    content TEXT,
        \\    created_at TEXT DEFAULT CURRENT_TIMESTAMP
        \\)
    );

    // Transacción con inserción de datos
    try db.begin();
    errdefer db.rollback() catch {};

    var insert_user = try db.prepare("INSERT INTO users (name, email) VALUES (?, ?)");
    defer insert_user.finalize();

    try insert_user.bindAll(.{ "Alice", "alice@example.com" });
    _ = try insert_user.step();
    const alice_id = db.lastInsertRowId();

    try insert_user.rebind(.{ "Bob", "bob@example.com" });
    _ = try insert_user.step();

    var insert_post = try db.prepare("INSERT INTO posts (user_id, title, content) VALUES (?, ?, ?)");
    defer insert_post.finalize();

    try insert_post.bindAll(.{ alice_id, "Hello World", "My first post!" });
    _ = try insert_post.step();

    try db.commit();

    // Consultar datos
    var query = try db.prepare(
        \\SELECT u.name, p.title, p.content
        \\FROM posts p
        \\JOIN users u ON u.id = p.user_id
        \\ORDER BY p.created_at DESC
    );
    defer query.finalize();

    std.debug.print("\nPosts:\n", .{});
    std.debug.print("-" ** 50 ++ "\n", .{});

    var iter = query.iterator();
    while (iter.next()) |row| {
        const author = row.text(0);
        const title = row.text(1);
        const content = row.text(2);

        std.debug.print("'{s}' by {s}\n", .{ title, author });
        std.debug.print("  {s}\n\n", .{content});
    }
}

Ejemplo con Mapeo a Estructuras

const User = struct {
    id: i64,
    name: []const u8,
    email: ?[]const u8,
};

var stmt = try db.prepare("SELECT id, name, email FROM users WHERE id = ?");
defer stmt.finalize();

try stmt.bindInt(1, user_id);

var iter = stmt.iterator();
if (iter.next()) |row| {
    const user = try row.toAlloc(User, allocator);
    defer row.freeStruct(User, user, allocator);

    std.debug.print("User: {s} <{s}>\n", .{
        user.name,
        user.email orelse "no email",
    });
}

Ejemplo con FTS5

var fts = sqlite.Fts5.init(&db, allocator);

try fts.createSimpleTable("articles", &.{ "title", "body" });

try db.exec("INSERT INTO articles (title, body) VALUES ('Zig Programming', 'Zig is a systems programming language...')");
try db.exec("INSERT INTO articles (title, body) VALUES ('SQLite Tutorial', 'SQLite is a lightweight database...')");

const results = try fts.searchWithHighlight(
    "articles",
    "programming",
    1, // body column
    "<mark>",
    "</mark>",
    10,
);
defer allocator.free(results);

for (results) |result| {
    std.debug.print("Match: {s}\n", .{result});
}

Ejemplo con Audit Log

var db = try sqlite.open("production.db");
defer db.close();

var audit = try sqlite.AuditLog.init(allocator, &db, .{
    .log_dir = "/var/log/app/audit",
    .app_name = "production_app",
});
defer audit.deinit();
audit.start();

// En cada request, establecer el usuario actual
try audit.setUser(current_user.id);

// Todas las operaciones quedan registradas automáticamente
try db.exec("UPDATE accounts SET balance = balance - 100 WHERE id = 1");

// Periódicamente verificar integridad
var result = try sqlite.verifyAuditChain(allocator, "/var/log/app/audit");
defer result.deinit(allocator);

if (!result.valid) {
    // ALERTA: Integridad comprometida
    log.err("Audit integrity check failed: {s}", .{result.error_message.?});
}

Información de Versión

const version_str = sqlite.version();      // "3.47.2"
const version_num = sqlite.versionNumber(); // 3047002

Licencia

Este proyecto está bajo licencia MIT. SQLite es de dominio público.


Generado para zcatsql v1.0.0 - Diciembre 2025