diff --git a/CLAUDE.md b/CLAUDE.md index 6167083..b0a2da7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,165 +1,406 @@ # zsqlite - SQLite Wrapper para Zig -> **Fecha creación**: 2025-12-08 -> **Versión Zig**: 0.15.2 -> **Estado**: En desarrollo inicial +> **Ultima actualizacion**: 2025-12-08 +> **Lenguaje**: Zig 0.15.2 +> **Estado**: v0.3 - Fase 2B completada +> **Inspiracion**: CGo go-sqlite3, SQLite C API -## Descripción del Proyecto +## Descripcion del Proyecto -Wrapper idiomático de SQLite para Zig. Compila SQLite amalgamation directamente en el binario, resultando en un ejecutable único sin dependencias externas. +**zsqlite** es un wrapper idiomatico de SQLite para Zig que compila SQLite amalgamation directamente en el binario, resultando en un ejecutable unico sin dependencias externas. -**Filosofía**: +**Filosofia**: - Zero dependencias runtime -- API idiomática Zig (errores, allocators, iteradores) -- Binario único y portable +- API idiomatica Zig (errores, allocators, iteradores) +- Binario unico y portable - Compatible con bases de datos SQLite existentes +- Calidad open source (doc comments, codigo claro) + +**Objetivo**: Ser el pilar para trabajar con databases en Zig con codigo 100% propio, replicando toda la funcionalidad de wrappers maduros como CGo go-sqlite3. + +--- + +## Estado Actual del Proyecto + +### Implementacion v0.3 (Fase 2B Completada) + +| Componente | Estado | Archivo | +|------------|--------|---------| +| **Core** | | | +| Database open/close | ✅ | `src/root.zig` | +| Database open with flags | ✅ | `src/root.zig` | +| exec() SQL simple | ✅ | `src/root.zig` | +| execAlloc() runtime strings | ✅ | `src/root.zig` | +| Error mapping completo | ✅ | `src/root.zig` | +| **Prepared Statements** | | | +| prepare/finalize | ✅ | `src/root.zig` | +| prepareAlloc() runtime strings | ✅ | `src/root.zig` | +| reset/clearBindings | ✅ | `src/root.zig` | +| step() iteration | ✅ | `src/root.zig` | +| sql() / isReadOnly() | ✅ | `src/root.zig` | +| parameterCount/Index/Name | ✅ | `src/root.zig` | +| **Bind Parameters** | | | +| bindNull | ✅ | `src/root.zig` | +| bindInt (i64) | ✅ | `src/root.zig` | +| bindFloat (f64) | ✅ | `src/root.zig` | +| bindText | ✅ | `src/root.zig` | +| bindBlob | ✅ | `src/root.zig` | +| bindBool | ✅ | `src/root.zig` | +| bindZeroblob | ✅ | `src/root.zig` | +| **Named Parameters** | | | +| bindNullNamed | ✅ | `src/root.zig` | +| bindIntNamed | ✅ | `src/root.zig` | +| bindFloatNamed | ✅ | `src/root.zig` | +| bindTextNamed | ✅ | `src/root.zig` | +| bindBlobNamed | ✅ | `src/root.zig` | +| bindBoolNamed | ✅ | `src/root.zig` | +| **Column Access** | | | +| columnCount | ✅ | `src/root.zig` | +| columnName | ✅ | `src/root.zig` | +| columnType | ✅ | `src/root.zig` | +| columnInt | ✅ | `src/root.zig` | +| columnFloat | ✅ | `src/root.zig` | +| columnText | ✅ | `src/root.zig` | +| columnBlob | ✅ | `src/root.zig` | +| columnIsNull | ✅ | `src/root.zig` | +| columnBool | ✅ | `src/root.zig` | +| columnBytes | ✅ | `src/root.zig` | +| columnDeclType | ✅ | `src/root.zig` | +| **Transacciones** | | | +| begin/commit/rollback | ✅ | `src/root.zig` | +| beginImmediate | ✅ | `src/root.zig` | +| beginExclusive | ✅ | `src/root.zig` | +| **Savepoints** | | | +| savepoint(name) | ✅ | `src/root.zig` | +| release(name) | ✅ | `src/root.zig` | +| rollbackTo(name) | ✅ | `src/root.zig` | +| **Pragmas/Config** | | | +| setBusyTimeout | ✅ | `src/root.zig` | +| setJournalMode | ✅ | `src/root.zig` | +| setSynchronous | ✅ | `src/root.zig` | +| enableWalMode | ✅ | `src/root.zig` | +| setForeignKeys | ✅ | `src/root.zig` | +| **ATTACH/DETACH** | | | +| attach(file, schema) | ✅ | `src/root.zig` | +| attachMemory(schema) | ✅ | `src/root.zig` | +| detach(schema) | ✅ | `src/root.zig` | +| listDatabases() | ✅ | `src/root.zig` | +| **Backup API** | | | +| Backup.init/initMain | ✅ | `src/root.zig` | +| Backup.step/stepAll | ✅ | `src/root.zig` | +| Backup.finish/deinit | ✅ | `src/root.zig` | +| Backup.remaining/pageCount | ✅ | `src/root.zig` | +| Backup.progress | ✅ | `src/root.zig` | +| backupDatabase() | ✅ | `src/root.zig` | +| backupToFile() | ✅ | `src/root.zig` | +| loadFromFile() | ✅ | `src/root.zig` | +| **User-Defined Functions** | | | +| createScalarFunction() | ✅ | `src/root.zig` | +| removeFunction() | ✅ | `src/root.zig` | +| FunctionContext (setInt/Float/Text/etc) | ✅ | `src/root.zig` | +| FunctionValue (asInt/Float/Text/etc) | ✅ | `src/root.zig` | +| **Custom Collations** | | | +| createCollation() | ✅ | `src/root.zig` | +| removeCollation() | ✅ | `src/root.zig` | +| **Utilidades** | | | +| lastInsertRowId | ✅ | `src/root.zig` | +| changes/totalChanges | ✅ | `src/root.zig` | +| errorMessage | ✅ | `src/root.zig` | +| errorCode/extendedErrorCode | ✅ | `src/root.zig` | +| interrupt | ✅ | `src/root.zig` | +| isReadOnly | ✅ | `src/root.zig` | +| filename | ✅ | `src/root.zig` | +| version/versionNumber | ✅ | `src/root.zig` | + +### Tests + +| Categoria | Tests | Estado | +|-----------|-------|--------| +| Version | 1 | ✅ | +| Open/Close | 1 | ✅ | +| Create/Insert | 1 | ✅ | +| Prepared Statements | 1 | ✅ | +| Select Query | 1 | ✅ | +| Transaction Commit | 1 | ✅ | +| Transaction Rollback | 1 | ✅ | +| Foreign Keys | 1 | ✅ | +| Null Values | 1 | ✅ | +| Blob Data | 1 | ✅ | +| Named Parameters | 1 | ✅ | +| Savepoints | 1 | ✅ | +| Busy Timeout | 1 | ✅ | +| Statement Metadata | 1 | ✅ | +| Boolean bind/column | 1 | ✅ | +| Backup memory to memory | 1 | ✅ | +| Backup convenience | 1 | ✅ | +| ATTACH/DETACH | 1 | ✅ | +| User-defined functions | 1 | ✅ | +| Custom collations | 1 | ✅ | +| **Total** | **20** | ✅ | + +--- + +## Roadmap + +### Fase 1 - Core (COMPLETADO) +- [x] Estructura proyecto +- [x] Compilar SQLite amalgamation +- [x] Abrir/cerrar bases de datos +- [x] Ejecutar SQL simple (exec) +- [x] Prepared statements basicos +- [x] Bind de parametros (int, text, blob, null, float) +- [x] Iterador de resultados +- [x] Transacciones basicas + +### Fase 2A - Paridad CGo Core (COMPLETADO) +- [x] SAVEPOINT support +- [x] Named parameters (:name, @name, $name) +- [x] Busy timeout +- [x] WAL mode helpers +- [x] Statement metadata (sql, isReadOnly, parameterCount) +- [x] Boolean bind/column +- [x] Error codes (errorCode, extendedErrorCode) +- [x] Database info (isReadOnly, filename) +- [x] interrupt() + +### Fase 2B - Paridad CGo Avanzada (COMPLETADO) +- [x] Backup API completo +- [x] User-defined functions (scalar) +- [x] Collations personalizadas +- [x] ATTACH/DETACH databases +- [ ] Batch bind con tuples/structs +- [ ] Row iterator idiomatico + +### Fase 3 - Avanzado (EN PROGRESO) +- [ ] Blob streaming (para archivos grandes) +- [ ] User-defined functions (aggregate) +- [ ] Authorizer callback +- [ ] Progress handler +- [ ] Update/Commit hooks +- [ ] Connection pooling + +### Fase 4 - Extras +- [ ] Connection URI parsing +- [ ] Virtual tables +- [ ] FTS5 helpers +- [ ] JSON1 helpers +- [ ] R-Tree helpers + +--- ## Arquitectura +### Estructura de Archivos + ``` zsqlite/ -├── CLAUDE.md # Este archivo -├── build.zig # Sistema de build +├── CLAUDE.md # Este archivo - estado del proyecto +├── build.zig # Sistema de build ├── src/ -│ ├── root.zig # Exports públicos -│ ├── sqlite.zig # Wrapper principal -│ ├── statement.zig # Prepared statements -│ ├── row.zig # Iterador de filas -│ └── errors.zig # Mapeo de errores SQLite +│ └── root.zig # Wrapper principal (~1000 lineas) ├── vendor/ -│ ├── sqlite3.c # SQLite amalgamation -│ └── sqlite3.h # Headers SQLite -└── examples/ - └── basic.zig # Ejemplo básico +│ ├── sqlite3.c # SQLite 3.47.2 amalgamation +│ ├── sqlite3.h # Headers SQLite +│ ├── sqlite3ext.h # Extension headers +│ └── shell.c # SQLite shell (no usado) +├── examples/ +│ └── basic.zig # Ejemplo basico funcional +└── docs/ + ├── ARCHITECTURE.md # Diseno interno + ├── API.md # Referencia rapida API + └── CGO_PARITY_ANALYSIS.md # Analisis de paridad con go-sqlite3 ``` -## API Objetivo +### SQLite Amalgamation -```zig -const std = @import("std"); -const sqlite = @import("zsqlite"); +**Version**: SQLite 3.47.2 -pub fn main() !void { - var db = try sqlite.open("test.db"); - defer db.close(); - - // Ejecutar SQL directo - try db.exec("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)"); - - // Prepared statement con parámetros - var stmt = try db.prepare("INSERT INTO users (name) VALUES (?)"); - defer stmt.deinit(); - - try stmt.bind(.{ "Alice" }); - try stmt.exec(); - - stmt.reset(); - try stmt.bind(.{ "Bob" }); - try stmt.exec(); - - // Query con iterador - var query = try db.prepare("SELECT id, name FROM users"); - defer query.deinit(); - - while (try query.step()) |row| { - const id = row.int(0); - const name = row.text(1); - std.debug.print("User {}: {s}\n", .{ id, name }); - } -} -``` - -## Funcionalidades Planificadas - -### Fase 1 - Core (Actual) -- [x] Estructura proyecto -- [ ] Compilar SQLite amalgamation -- [ ] Abrir/cerrar bases de datos -- [ ] Ejecutar SQL simple (exec) -- [ ] Prepared statements básicos -- [ ] Bind de parámetros (int, text, blob, null) -- [ ] Iterador de resultados - -### Fase 2 - Transacciones y Errores -- [ ] BEGIN/COMMIT/ROLLBACK helpers -- [ ] Mapeo completo errores SQLite -- [ ] SAVEPOINT support - -### Fase 3 - Avanzado -- [ ] Blob streaming (para archivos grandes) -- [ ] User-defined functions -- [ ] Collations personalizadas -- [ ] Backup API - -## SQLite Amalgamation - -Usamos SQLite amalgamation (sqlite3.c + sqlite3.h) que es la forma recomendada de embeber SQLite. Es un único archivo .c con todo el código de SQLite. - -**Versión objetivo**: SQLite 3.45+ (última estable) - -**Flags de compilación recomendados**: +**Flags de compilacion**: ``` -DSQLITE_DQS=0 # Disable double-quoted strings --DSQLITE_THREADSAFE=0 # Single-threaded (más rápido) +-DSQLITE_THREADSAFE=0 # Single-threaded (mas rapido) -DSQLITE_DEFAULT_MEMSTATUS=0 # Disable memory tracking -DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1 -DSQLITE_LIKE_DOESNT_MATCH_BLOBS -DSQLITE_OMIT_DEPRECATED -DSQLITE_OMIT_SHARED_CACHE --DSQLITE_USE_ALLOCA -DSQLITE_ENABLE_FTS5 # Full-text search -DSQLITE_ENABLE_JSON1 # JSON functions +-DSQLITE_ENABLE_RTREE # R-Tree geospatial +-DSQLITE_OMIT_LOAD_EXTENSION # No dynamic extensions ``` -## Compilación - -```bash -# Descargar SQLite amalgamation (una sola vez) -cd vendor -curl -O https://sqlite.org/2024/sqlite-amalgamation-3450000.zip -unzip sqlite-amalgamation-3450000.zip -mv sqlite-amalgamation-3450000/* . -rm -rf sqlite-amalgamation-3450000* - -# Compilar -zig build - -# Ejecutar tests -zig build test - -# Ejecutar ejemplo -zig build example -``` - -## Diferencias con otros wrappers - -| Característica | zsqlite | zig-sqlite | zqlite.zig | -|----------------|---------|------------|------------| -| Compila SQLite | Sí | No (linkea) | Sí | -| API idiomática | Sí | Parcial | Sí | -| Zero alloc option | Planificado | No | No | -| Zig 0.15 | Sí | ? | ? | - -## Uso en simifactu-zig (futuro) - -Este wrapper está diseñado para ser drop-in replacement del uso de SQLite en simifactu-fyne, soportando: -- Foreign keys -- Transactions -- Prepared statements -- BLOB storage -- JSON functions - --- -## Equipo y Metodología +## API Actual -### Normas de Trabajo +### Abrir Base de Datos -**IMPORTANTE**: Todas las normas de trabajo están en: +```zig +const sqlite = @import("zsqlite"); + +// In-memory +var db = try sqlite.openMemory(); +defer db.close(); + +// Archivo +var db = try sqlite.open("test.db"); +defer db.close(); + +// Con flags +var db = try sqlite.Database.openWithFlags("test.db", .{ + .read_only = true, + .create = false, +}); +defer db.close(); +``` + +### Ejecutar SQL Simple + +```zig +try db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)"); +try db.exec("INSERT INTO users (name) VALUES ('Alice')"); + +// Con string runtime +try db.execAlloc(allocator, sql_string); +``` + +### Prepared Statements + +```zig +var stmt = try db.prepare("INSERT INTO users (name, age) VALUES (?, ?)"); +defer stmt.finalize(); + +try stmt.bindText(1, "Bob"); +try stmt.bindInt(2, 30); +_ = try stmt.step(); + +// Reusar +try stmt.reset(); +try stmt.clearBindings(); +try stmt.bindText(1, "Charlie"); +try stmt.bindInt(2, 25); +_ = try stmt.step(); +``` + +### Named Parameters (NUEVO v0.2) + +```zig +var stmt = try db.prepare("INSERT INTO users (name, age) VALUES (:name, :age)"); +defer stmt.finalize(); + +try stmt.bindTextNamed(":name", "Alice"); +try stmt.bindIntNamed(":age", 30); +_ = try stmt.step(); + +// Tambien soporta @name y $name estilos +``` + +### Queries + +```zig +var stmt = try db.prepare("SELECT id, name FROM users ORDER BY id"); +defer stmt.finalize(); + +while (try stmt.step()) { + const id = stmt.columnInt(0); + const name = stmt.columnText(1) orelse "(null)"; + std.debug.print("User {}: {s}\n", .{ id, name }); +} +``` + +### Transacciones + +```zig +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(); +``` + +### Savepoints (NUEVO v0.2) + +```zig +const allocator = std.heap.page_allocator; + +try db.begin(); +try db.savepoint(allocator, "sp1"); + +try db.exec("INSERT INTO test VALUES (1)"); + +// Revertir al savepoint +try db.rollbackTo(allocator, "sp1"); + +// O confirmar el savepoint +try db.release(allocator, "sp1"); + +try db.commit(); +``` + +### WAL Mode (NUEVO v0.2) + +```zig +const allocator = std.heap.page_allocator; + +// Habilitar WAL mode con settings recomendados +try db.enableWalMode(allocator); + +// O configurar individualmente +try db.setJournalMode(allocator, "WAL"); +try db.setSynchronous(allocator, "NORMAL"); +try db.setBusyTimeout(5000); // 5 segundos +``` + +### Foreign Keys + +```zig +try db.setForeignKeys(true); +``` + +--- + +## Comandos + +```bash +# Zig path +ZIG=/mnt/cello2/arno/re/recode/zig/zig-0.15.2/zig-x86_64-linux-0.15.2/zig + +# Compilar +$ZIG build + +# Tests +$ZIG build test + +# Ejecutar ejemplo +$ZIG build basic && ./zig-out/bin/basic +``` + +--- + +## Equipo y Metodologia + +### Normas de Trabajo Centralizadas + +**IMPORTANTE**: Todas las normas de trabajo estan en: ``` /mnt/cello2/arno/re/recode/TEAM_STANDARDS/ ``` +**Archivos clave a leer**: +- `LAST_UPDATE.md` - **LEER PRIMERO** - Cambios recientes en normas +- `NORMAS_TRABAJO_CONSENSUADAS.md` - Metodologia fundamental +- `QUICK_REFERENCE.md` - Cheat sheet rapido + +### Estandares Zig Open Source (Seccion #24) + +- **Claridad**: Codigo autoexplicativo, nombres descriptivos +- **Doc comments**: `///` en todas las funciones publicas +- **Idiomatico**: snake_case, error handling explicito +- **Sin magia**: Preferir codigo explicito sobre abstracciones complejas + ### Control de Versiones ```bash @@ -167,18 +408,92 @@ Este wrapper está diseñado para ser drop-in replacement del uso de SQLite en s git remote: git@git.reugenio.com:reugenio/zsqlite.git # Branches -main # Código estable -develop # Desarrollo activo +main # Codigo estable ``` -### Zig Path +--- -```bash -ZIG=/mnt/cello2/arno/re/recode/zig/zig-0.15.2/zig-x86_64-linux-0.15.2/zig -``` +## Referencias + +### CGo go-sqlite3 (Referencia principal) +- Repo: https://github.com/mattn/go-sqlite3 +- Objetivo: Replicar toda su funcionalidad en Zig +- Analisis: `docs/CGO_PARITY_ANALYSIS.md` + +### SQLite C API +- Docs: https://sqlite.org/c3ref/intro.html +- Objetivo: Wrapper completo de funciones relevantes + +### Otros Wrappers Zig (Referencia) +- zig-sqlite: https://github.com/vrischmann/zig-sqlite +- zqlite.zig: https://github.com/karlseguin/zqlite.zig + +--- + +## Historial de Desarrollo + +### 2025-12-08 - v0.3 (Fase 2B Completada) +- **Backup API completo**: + - Backup struct con init/step/finish/remaining/pageCount/progress + - Funciones de conveniencia: backupDatabase, backupToFile, loadFromFile +- **User-Defined Functions (Scalar)**: + - createScalarFunction() para registrar funciones Zig en SQL + - FunctionContext para retornar resultados + - FunctionValue para acceder a argumentos + - Cleanup automatico con destructores +- **Custom Collations**: + - createCollation() para orden personalizado de strings + - removeCollation() para eliminar collations +- **ATTACH/DETACH Databases**: + - attach(), attachMemory(), detach() + - listDatabases() para enumerar schemas adjuntos +- 20 tests pasando +- ~1700 lineas de codigo en root.zig + +### 2025-12-08 - v0.2 (Fase 2A Completada) +- Named parameters (:name, @name, $name) +- Savepoints (savepoint, release, rollbackTo) +- WAL mode helpers (enableWalMode, setJournalMode, setSynchronous) +- Busy timeout (setBusyTimeout) +- Statement metadata (sql, isReadOnly, parameterCount, parameterIndex, parameterName) +- Boolean bind/column (bindBool, columnBool) +- Error codes (errorCode, extendedErrorCode) +- Database info (isReadOnly, filename, interrupt) +- bindZeroblob, columnBytes, columnDeclType +- 15 tests pasando +- Documentacion docs/ creada (ARCHITECTURE.md, API.md, CGO_PARITY_ANALYSIS.md) + +### 2025-12-08 - v0.1 (Core Funcional) +- Estructura inicial del proyecto +- SQLite 3.47.2 amalgamation compilando +- Database, Statement, Error types +- Bind parameters completos (null, int, float, text, blob) +- Column access completo +- Transacciones basicas +- 10 tests unitarios pasando +- Ejemplo basic.zig funcional --- ## Notas de Desarrollo -*Se irán añadiendo conforme avance el proyecto* +### Proxima Sesion +1. Blob streaming (para archivos grandes) +2. User-defined aggregate functions +3. Update/Commit hooks +4. Batch bind con comptime structs + +### Lecciones Aprendidas +- SQLite amalgamation compila limpiamente con Zig build system +- `@cImport` funciona bien para headers SQLite +- SQLITE_TRANSIENT necesario para strings/blobs que Zig puede mover +- En Zig 0.15 usar `std.fmt.allocPrint` + `\x00` manual en lugar de `allocPrintZ` +- Named parameters funcionan con el prefijo incluido (":name", no "name") +- Callbacks C requieren `callconv(.c)` y estructuras wrapper para pasar estado +- `std.ArrayListUnmanaged` en Zig 0.15 (no `ArrayList.init`) +- UDFs y Collations usan page_allocator interno para simplicidad + +--- + +**© zsqlite - Wrapper SQLite para Zig** +*2025-12-08 - En desarrollo activo* diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..1d1772e --- /dev/null +++ b/docs/API.md @@ -0,0 +1,533 @@ +# zsqlite - API Reference + +> **Version**: 0.3 +> **Ultima actualizacion**: 2025-12-08 + +## Quick Reference + +```zig +const sqlite = @import("zsqlite"); + +// Abrir base de datos +var db = try sqlite.openMemory(); // In-memory +var db = try sqlite.open("file.db"); // Archivo +defer db.close(); + +// SQL directo +try db.exec("CREATE TABLE ..."); + +// Prepared statement +var stmt = try db.prepare("SELECT * FROM users WHERE id = ?"); +defer stmt.finalize(); +try stmt.bindInt(1, 42); +while (try stmt.step()) { + const name = stmt.columnText(1); +} + +// Named parameters +var stmt = try db.prepare("INSERT INTO users VALUES (:name, :age)"); +try stmt.bindTextNamed(":name", "Alice"); +try stmt.bindIntNamed(":age", 30); + +// Transacciones +try db.begin(); +// ... operaciones ... +try db.commit(); // o db.rollback() + +// Savepoints +try db.savepoint(allocator, "sp1"); +try db.rollbackTo(allocator, "sp1"); +try db.release(allocator, "sp1"); + +// Backup +try sqlite.backupToFile(&db, "backup.db"); +var restored = try sqlite.loadFromFile("backup.db"); + +// User-defined functions +try db.createScalarFunction("double", 1, myDoubleFunc); + +// Custom collations +try db.createCollation("NOCASE2", myCaseInsensitiveCompare); +``` + +--- + +## Funciones de Modulo + +### open + +```zig +pub fn open(path: [:0]const u8) Error!Database +``` + +Abre una base de datos SQLite. + +**Parametros:** +- `path`: Ruta al archivo. Usar `":memory:"` para base de datos en memoria. + +**Retorna:** `Database` o error. + +--- + +### openMemory + +```zig +pub fn openMemory() Error!Database +``` + +Abre una base de datos en memoria. + +--- + +### version / versionNumber + +```zig +pub fn version() []const u8 +pub fn versionNumber() i32 +``` + +Retorna la version de SQLite como string ("3.47.2") o numero (3047002). + +--- + +### backupDatabase + +```zig +pub fn backupDatabase(dest_db: *Database, source_db: *Database) Error!void +``` + +Copia una base de datos completa a otra. + +--- + +### backupToFile + +```zig +pub fn backupToFile(source_db: *Database, path: [:0]const u8) Error!void +``` + +Guarda una base de datos a un archivo. + +--- + +### loadFromFile + +```zig +pub fn loadFromFile(path: [:0]const u8) Error!Database +``` + +Carga una base de datos desde archivo a memoria. + +--- + +## Database + +### Apertura y Cierre + +| Funcion | Descripcion | +|---------|-------------| +| `open(path)` | Abre conexion (read-write, create) | +| `openWithFlags(path, flags)` | Abre con flags especificos | +| `close()` | Cierra la conexion | + +### Ejecucion SQL + +| Funcion | Descripcion | +|---------|-------------| +| `exec(sql)` | Ejecuta SQL sin resultados | +| `execAlloc(alloc, sql)` | exec con string runtime | +| `prepare(sql)` | Crea prepared statement | +| `prepareAlloc(alloc, sql)` | prepare con string runtime | + +### Transacciones + +| Funcion | Descripcion | +|---------|-------------| +| `begin()` | Inicia transaccion (DEFERRED) | +| `beginImmediate()` | Inicia con lock inmediato | +| `beginExclusive()` | Inicia con lock exclusivo | +| `commit()` | Confirma transaccion | +| `rollback()` | Revierte transaccion | + +### Savepoints + +```zig +pub fn savepoint(self: *Database, allocator: Allocator, name: []const u8) !void +pub fn release(self: *Database, allocator: Allocator, name: []const u8) !void +pub fn rollbackTo(self: *Database, allocator: Allocator, name: []const u8) !void +``` + +Savepoints permiten transacciones anidadas. + +**Ejemplo:** +```zig +try db.begin(); +try db.savepoint(alloc, "sp1"); +try db.exec("INSERT ..."); +try db.rollbackTo(alloc, "sp1"); // Revierte INSERT +try db.release(alloc, "sp1"); +try db.commit(); +``` + +### Configuracion + +| Funcion | Descripcion | +|---------|-------------| +| `setForeignKeys(enabled)` | Habilita/deshabilita FKs | +| `setBusyTimeout(ms)` | Timeout en ms para locks | +| `setJournalMode(alloc, mode)` | "WAL", "DELETE", etc | +| `setSynchronous(alloc, mode)` | "OFF", "NORMAL", "FULL" | +| `enableWalMode(alloc)` | WAL + NORMAL sync | + +### ATTACH/DETACH + +```zig +pub fn attach(self: *Database, alloc: Allocator, path: []const u8, schema: []const u8) !void +pub fn attachMemory(self: *Database, alloc: Allocator, schema: []const u8) !void +pub fn detach(self: *Database, alloc: Allocator, schema: []const u8) !void +pub fn listDatabases(self: *Database, alloc: Allocator) ![][]const u8 +pub fn freeDatabaseList(alloc: Allocator, list: [][]const u8) void +``` + +**Ejemplo:** +```zig +try db.attachMemory(alloc, "cache"); +try db.exec("CREATE TABLE cache.items (...)"); +// SELECT * FROM cache.items +try db.detach(alloc, "cache"); +``` + +### User-Defined Functions + +```zig +pub fn createScalarFunction( + self: *Database, + name: [:0]const u8, + num_args: i32, + func: ScalarFn, +) !void + +pub fn removeFunction(self: *Database, name: [:0]const u8, num_args: i32) Error!void +``` + +**Ejemplo:** +```zig +fn myDouble(ctx: FunctionContext, args: []const FunctionValue) void { + if (args[0].isNull()) { + ctx.setNull(); + return; + } + ctx.setInt(args[0].asInt() * 2); +} + +try db.createScalarFunction("double", 1, myDouble); +// SELECT double(value) FROM table +``` + +### Custom Collations + +```zig +pub fn createCollation(self: *Database, name: [:0]const u8, func: CollationFn) !void +pub fn removeCollation(self: *Database, name: [:0]const u8) Error!void +``` + +**Ejemplo:** +```zig +fn reverseOrder(a: []const u8, b: []const u8) i32 { + return -std.mem.order(u8, a, b); +} + +try db.createCollation("REVERSE", reverseOrder); +// SELECT * FROM table ORDER BY name COLLATE REVERSE +``` + +### Utilidades + +| Funcion | Descripcion | +|---------|-------------| +| `lastInsertRowId()` | Ultimo rowid insertado | +| `changes()` | Filas modificadas (ultimo stmt) | +| `totalChanges()` | Total filas desde conexion | +| `errorMessage()` | Mensaje de error reciente | +| `errorCode()` | Codigo de error | +| `extendedErrorCode()` | Codigo extendido | +| `interrupt()` | Interrumpe operacion | +| `isReadOnly(db_name)` | Si DB es readonly | +| `filename(db_name)` | Ruta del archivo | + +--- + +## Statement + +### Ciclo de Vida + +| Funcion | Descripcion | +|---------|-------------| +| `finalize()` | Libera el statement | +| `reset()` | Resetea para re-ejecucion | +| `clearBindings()` | Limpia parametros | +| `step()` | Ejecuta un paso (true=hay fila) | + +### Metadata + +| Funcion | Descripcion | +|---------|-------------| +| `sql()` | Texto SQL del statement | +| `isReadOnly()` | Si es SELECT | +| `parameterCount()` | Numero de parametros | +| `parameterIndex(name)` | Indice de parametro named | +| `parameterName(index)` | Nombre de parametro | + +### Bind Parameters (1-indexed) + +| Funcion | Descripcion | +|---------|-------------| +| `bindNull(idx)` | NULL | +| `bindInt(idx, val)` | i64 | +| `bindFloat(idx, val)` | f64 | +| `bindText(idx, val)` | []const u8 | +| `bindBlob(idx, val)` | []const u8 | +| `bindBool(idx, val)` | bool (como 0/1) | +| `bindZeroblob(idx, size)` | Blob de ceros | + +### Named Parameters + +| Funcion | Descripcion | +|---------|-------------| +| `bindNullNamed(name)` | `:name`, `@name`, `$name` | +| `bindIntNamed(name, val)` | | +| `bindFloatNamed(name, val)` | | +| `bindTextNamed(name, val)` | | +| `bindBlobNamed(name, val)` | | +| `bindBoolNamed(name, val)` | | + +### Column Access (0-indexed) + +| Funcion | Descripcion | +|---------|-------------| +| `columnCount()` | Numero de columnas | +| `columnName(idx)` | Nombre | +| `columnType(idx)` | ColumnType enum | +| `columnInt(idx)` | i64 | +| `columnFloat(idx)` | f64 | +| `columnText(idx)` | ?[]const u8 | +| `columnBlob(idx)` | ?[]const u8 | +| `columnBool(idx)` | bool | +| `columnIsNull(idx)` | bool | +| `columnBytes(idx)` | Tamano en bytes | +| `columnDeclType(idx)` | Tipo declarado | + +--- + +## Backup + +```zig +pub const Backup = struct { + pub fn init(dest: *Database, dest_name: [:0]const u8, + source: *Database, source_name: [:0]const u8) Error!Backup + pub fn initMain(dest: *Database, source: *Database) Error!Backup + pub fn step(self: *Backup, n_pages: i32) Error!bool + pub fn stepAll(self: *Backup) Error!void + pub fn remaining(self: *Backup) i32 + pub fn pageCount(self: *Backup) i32 + pub fn progress(self: *Backup) u8 // 0-100 + pub fn finish(self: *Backup) Error!void + pub fn deinit(self: *Backup) void +}; +``` + +**Ejemplo con progreso:** +```zig +var backup = try Backup.initMain(&dest_db, &source_db); +defer backup.deinit(); + +while (try backup.step(100)) { + std.debug.print("Progress: {}%\n", .{backup.progress()}); +} +``` + +--- + +## User-Defined Functions + +### FunctionContext + +```zig +pub const FunctionContext = struct { + pub fn setNull(self: Self) void + pub fn setInt(self: Self, value: i64) void + pub fn setFloat(self: Self, value: f64) void + pub fn setText(self: Self, value: []const u8) void + pub fn setBlob(self: Self, value: []const u8) void + pub fn setError(self: Self, msg: []const u8) void +}; +``` + +### FunctionValue + +```zig +pub const FunctionValue = struct { + pub fn getType(self: Self) ColumnType + pub fn isNull(self: Self) bool + pub fn asInt(self: Self) i64 + pub fn asFloat(self: Self) f64 + pub fn asText(self: Self) ?[]const u8 + pub fn asBlob(self: Self) ?[]const u8 +}; +``` + +### ScalarFn Type + +```zig +pub const ScalarFn = *const fn (ctx: FunctionContext, args: []const FunctionValue) void; +``` + +--- + +## Types + +### OpenFlags + +```zig +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, +}; +``` + +### ColumnType + +```zig +pub const ColumnType = enum { + integer, + float, + text, + blob, + null_value, +}; +``` + +### CollationFn + +```zig +pub const CollationFn = *const fn (a: []const u8, b: []const u8) i32; +``` + +Retorna: negativo si a < b, cero si a == b, positivo si a > b. + +### Error + +Mapeo completo de codigos SQLite: + +| Error | Descripcion | +|-------|-------------| +| `SqliteError` | Error generico | +| `Busy` | Database bloqueada | +| `Locked` | Tabla bloqueada | +| `Constraint` | Violacion de constraint | +| `OutOfMemory` | Sin memoria | +| `IoError` | Error de I/O | +| `Corrupt` | DB corrupta | +| `CantOpen` | No se puede abrir | +| `ReadOnly` | DB es readonly | +| `Range` | Parametro fuera de rango | +| ... | (25+ errores mapeados) | + +--- + +## Ejemplos Completos + +### CRUD con Named Parameters + +```zig +var db = try sqlite.openMemory(); +defer db.close(); + +try db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)"); + +// Insert con named params +var insert = try db.prepare("INSERT INTO users (name, age) VALUES (:name, :age)"); +defer insert.finalize(); + +try insert.bindTextNamed(":name", "Alice"); +try insert.bindIntNamed(":age", 30); +_ = try insert.step(); + +// Query +var query = try db.prepare("SELECT * FROM users WHERE age > :min_age"); +defer query.finalize(); + +try query.bindIntNamed(":min_age", 25); +while (try query.step()) { + const name = query.columnText(1) orelse "(null)"; + std.debug.print("User: {s}\n", .{name}); +} +``` + +### Backup con Progreso + +```zig +var source = try sqlite.open("production.db"); +defer source.close(); + +var dest = try sqlite.open("backup.db"); +defer dest.close(); + +var backup = try sqlite.Backup.initMain(&dest, &source); +defer backup.deinit(); + +while (try backup.step(100)) { + const pct = backup.progress(); + std.debug.print("\rBackup: {d}%", .{pct}); +} +std.debug.print("\nBackup complete!\n", .{}); +``` + +### UDF: String Length + +```zig +fn strLen(ctx: sqlite.FunctionContext, args: []const sqlite.FunctionValue) void { + if (args.len != 1 or args[0].isNull()) { + ctx.setNull(); + return; + } + if (args[0].asText()) |text| { + ctx.setInt(@intCast(text.len)); + } else { + ctx.setNull(); + } +} + +try db.createScalarFunction("strlen", 1, strLen); +// SELECT strlen(name) FROM users +``` + +### Collation: Case-Insensitive + +```zig +fn caseInsensitive(a: []const u8, b: []const u8) i32 { + var i: usize = 0; + while (i < a.len and i < b.len) : (i += 1) { + const ca = std.ascii.toLower(a[i]); + const cb = std.ascii.toLower(b[i]); + if (ca < cb) return -1; + if (ca > cb) return 1; + } + if (a.len < b.len) return -1; + if (a.len > b.len) return 1; + return 0; +} + +try db.createCollation("ICASE", caseInsensitive); +// SELECT * FROM users ORDER BY name COLLATE ICASE +``` + +--- + +**© zsqlite v0.3 - API Reference** +*2025-12-08* diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..88667c1 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,397 @@ +# zsqlite - Arquitectura Tecnica + +> **Version**: 0.3 +> **Ultima actualizacion**: 2025-12-08 + +## Vision General + +zsqlite es un wrapper de SQLite para Zig que compila SQLite amalgamation directamente en el binario. El objetivo es proveer una API idiomatica Zig mientras se mantiene acceso completo a las capacidades de SQLite. + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Aplicacion Zig │ +├─────────────────────────────────────────────────────────────┤ +│ zsqlite API │ +│ ┌──────────┐ ┌───────────┐ ┌──────────┐ ┌───────────┐ │ +│ │ Database │ │ Statement │ │ Error │ │ Column │ │ +│ │ │ │ │ │ Mapping │ │ Type │ │ +│ └────┬─────┘ └─────┬─────┘ └────┬─────┘ └─────┬─────┘ │ +├───────┼──────────────┼─────────────┼──────────────┼────────┤ +│ │ │ │ │ │ +│ └──────────────┴─────────────┴──────────────┘ │ +│ @cImport │ +├─────────────────────────────────────────────────────────────┤ +│ SQLite 3.47.2 (C) │ +│ (amalgamation) │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Componentes Principales + +### 1. Database + +Representa una conexion a una base de datos SQLite. + +```zig +pub const Database = struct { + handle: ?*c.sqlite3, + + pub fn open(path: [:0]const u8) Error!Self { ... } + pub fn openWithFlags(path: [:0]const u8, flags: OpenFlags) Error!Self { ... } + pub fn close(self: *Self) void { ... } + pub fn exec(self: *Self, sql: [:0]const u8) Error!void { ... } + pub fn prepare(self: *Self, sql: [:0]const u8) Error!Statement { ... } + // ... +}; +``` + +**Responsabilidades:** +- Gestionar ciclo de vida de conexion SQLite +- Ejecutar SQL directo (exec) +- Crear prepared statements +- Gestionar transacciones +- Configurar pragmas (foreign keys, etc.) + +### 2. Statement + +Representa un prepared statement de SQLite. + +```zig +pub const Statement = struct { + handle: ?*c.sqlite3_stmt, + db: *Database, + + pub fn finalize(self: *Self) void { ... } + pub fn reset(self: *Self) Error!void { ... } + pub fn step(self: *Self) Error!bool { ... } + pub fn bindInt(self: *Self, index: u32, value: i64) Error!void { ... } + pub fn bindTextNamed(self: *Self, name: [:0]const u8, value: []const u8) Error!void { ... } + pub fn columnText(self: *Self, index: u32) ?[]const u8 { ... } + // ... +}; +``` + +**Responsabilidades:** +- Bind de parametros posicionales (1-indexed) +- Bind de parametros named (:name, @name, $name) +- Ejecucion paso a paso (step) +- Acceso a columnas de resultados +- Metadata (sql, isReadOnly, parameterCount, etc.) +- Reset para reutilizacion + +### 3. Error + +Mapeo de codigos de error SQLite a errores Zig. + +```zig +pub const Error = error{ + SqliteError, + InternalError, + PermissionDenied, + Busy, + Locked, + OutOfMemory, + // ... 25+ errores mapeados +}; + +fn resultToError(result: c_int) Error { ... } +``` + +**Diseno:** +- Cada codigo SQLite tiene su propio error Zig +- Permite manejo especifico por tipo de error +- SQLITE_ROW y SQLITE_DONE manejados especialmente en step() + +### 4. OpenFlags + +Configuracion para apertura de base de datos. + +```zig +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, +}; +``` + +### 5. ColumnType + +Enum para tipos de columna SQLite. + +```zig +pub const ColumnType = enum { + integer, + float, + text, + blob, + null_value, +}; +``` + +### 6. Backup + +API para copiar bases de datos con control de progreso. + +```zig +pub const Backup = struct { + handle: ?*c.sqlite3_backup, + dest_db: *Database, + source_db: *Database, + + pub fn init(dest: *Database, dest_name: [:0]const u8, + source: *Database, source_name: [:0]const u8) Error!Self { ... } + pub fn step(self: *Self, n_pages: i32) Error!bool { ... } + pub fn stepAll(self: *Self) Error!void { ... } + pub fn remaining(self: *Self) i32 { ... } + pub fn pageCount(self: *Self) i32 { ... } + pub fn progress(self: *Self) u8 { ... } + pub fn finish(self: *Self) Error!void { ... } + pub fn deinit(self: *Self) void { ... } +}; +``` + +**Responsabilidades:** +- Copiar base de datos pagina por pagina +- Control granular del progreso +- Permitir backups incrementales + +### 7. User-Defined Functions + +Soporte para funciones personalizadas en SQL. + +```zig +pub const FunctionContext = struct { + ctx: *c.sqlite3_context, + + pub fn setNull(self: Self) void { ... } + pub fn setInt(self: Self, value: i64) void { ... } + pub fn setFloat(self: Self, value: f64) void { ... } + pub fn setText(self: Self, value: []const u8) void { ... } + pub fn setBlob(self: Self, value: []const u8) void { ... } + pub fn setError(self: Self, msg: []const u8) void { ... } +}; + +pub const FunctionValue = struct { + value: *c.sqlite3_value, + + pub fn getType(self: Self) ColumnType { ... } + pub fn isNull(self: Self) bool { ... } + pub fn asInt(self: Self) i64 { ... } + pub fn asFloat(self: Self) f64 { ... } + pub fn asText(self: Self) ?[]const u8 { ... } + pub fn asBlob(self: Self) ?[]const u8 { ... } +}; + +pub const ScalarFn = *const fn (ctx: FunctionContext, args: []const FunctionValue) void; +``` + +**Patron de uso:** +```zig +fn myDouble(ctx: FunctionContext, args: []const FunctionValue) void { + if (args[0].isNull()) { + ctx.setNull(); + return; + } + ctx.setInt(args[0].asInt() * 2); +} + +try db.createScalarFunction("double", 1, myDouble); +// SELECT double(5) => 10 +``` + +### 8. Custom Collations + +Soporte para ordenamiento personalizado. + +```zig +pub const CollationFn = *const fn (a: []const u8, b: []const u8) i32; +``` + +**Patron de uso:** +```zig +fn reverseOrder(a: []const u8, b: []const u8) i32 { + return -std.mem.order(u8, a, b); +} + +try db.createCollation("REVERSE", reverseOrder); +// SELECT * FROM t ORDER BY col COLLATE REVERSE +``` + +## Integracion con SQLite C + +### @cImport + +Usamos `@cImport` para importar los headers de SQLite: + +```zig +const c = @cImport({ + @cInclude("sqlite3.h"); +}); +``` + +Esto nos da acceso directo a: +- `c.sqlite3` - Handle de conexion +- `c.sqlite3_stmt` - Handle de statement +- `c.sqlite3_open()`, `c.sqlite3_close()`, etc. +- Todas las constantes (`c.SQLITE_OK`, `c.SQLITE_ROW`, etc.) + +### SQLITE_TRANSIENT + +Para strings y blobs, usamos `c.SQLITE_TRANSIENT` que indica a SQLite que debe copiar los datos: + +```zig +const result = c.sqlite3_bind_text( + self.handle, + @intCast(index), + value.ptr, + @intCast(value.len), + c.SQLITE_TRANSIENT, // SQLite copia el string +); +``` + +Esto es necesario porque Zig puede mover/liberar la memoria del slice original. + +### Conversion de Indices + +SQLite usa diferentes convenciones de indices: +- **Bind parameters**: 1-indexed (SQLite convencion) +- **Column access**: 0-indexed (SQLite convencion) + +Mantenemos las mismas convenciones en la API publica para consistencia con documentacion SQLite. + +## Build System + +### build.zig + +```zig +// Compilar SQLite como C source +zsqlite_mod.addCSourceFile(.{ + .file = b.path("vendor/sqlite3.c"), + .flags = sqlite_flags, +}); + +// Flags de optimizacion +const sqlite_flags: []const []const u8 = &.{ + "-DSQLITE_DQS=0", + "-DSQLITE_THREADSAFE=0", + "-DSQLITE_DEFAULT_MEMSTATUS=0", + // ... +}; +``` + +### Flags de Compilacion + +| Flag | Proposito | +|------|-----------| +| `SQLITE_DQS=0` | Deshabilita double-quoted strings como identificadores | +| `SQLITE_THREADSAFE=0` | Single-threaded (mas rapido) | +| `SQLITE_DEFAULT_MEMSTATUS=0` | Sin tracking de memoria | +| `SQLITE_ENABLE_FTS5` | Full-text search habilitado | +| `SQLITE_ENABLE_JSON1` | Funciones JSON habilitadas | +| `SQLITE_ENABLE_RTREE` | R-Tree para geospatial | +| `SQLITE_OMIT_LOAD_EXTENSION` | Sin extensiones dinamicas (seguridad) | + +## Patrones de Uso + +### Patron RAII con defer + +```zig +var db = try sqlite.openMemory(); +defer db.close(); + +var stmt = try db.prepare("SELECT * FROM users"); +defer stmt.finalize(); +``` + +### Patron Transaccion con errdefer + +```zig +try db.begin(); +errdefer db.rollback() catch {}; + +try db.exec("INSERT ..."); +try db.exec("UPDATE ..."); + +try db.commit(); +``` + +### Iteracion de Resultados + +```zig +while (try stmt.step()) { + const id = stmt.columnInt(0); + const name = stmt.columnText(1) orelse "(null)"; + // procesar fila +} +``` + +## Decisiones de Diseno + +### 1. Todo en root.zig (por ahora) + +Para v0.1, todo el codigo esta en un solo archivo. Cuando crezca significativamente (>400 lineas core), se fragmentara en: +- `database.zig` +- `statement.zig` +- `errors.zig` +- `types.zig` + +### 2. Error Union vs Nullable + +- Operaciones que pueden fallar: `Error!T` +- Operaciones de lectura que pueden ser NULL: `?T` + +```zig +// Puede fallar (error de SQLite) +pub fn open(path: [:0]const u8) Error!Self + +// Puede ser NULL (columna NULL) +pub fn columnText(self: *Self, index: u32) ?[]const u8 +``` + +### 3. Slices vs Null-Terminated + +- API publica acepta `[:0]const u8` para strings SQL (null-terminated) +- Variantes `*Alloc` aceptan `[]const u8` y agregan null terminator + +### 4. Indices Consistentes con SQLite + +Mantenemos las convenciones de SQLite: +- Bind: 1-indexed +- Columns: 0-indexed + +Esto facilita traducir ejemplos de documentacion SQLite. + +## Roadmap Arquitectural + +### Fase 2: Modularizacion + +Cuando el codigo crezca, fragmentar en modulos: +``` +src/ +├── root.zig # Re-exports publicos +├── database.zig # Database struct +├── statement.zig # Statement struct +├── errors.zig # Error types +├── types.zig # OpenFlags, ColumnType +└── c.zig # @cImport centralizado +``` + +### Fase 3: Features Avanzadas + +``` +src/ +├── ... +├── blob.zig # Blob streaming +├── functions.zig # User-defined functions +├── backup.zig # Backup API +└── hooks.zig # Update/commit hooks +``` + +--- + +**© zsqlite - Arquitectura Tecnica** +*2025-12-08* diff --git a/docs/CGO_PARITY_ANALYSIS.md b/docs/CGO_PARITY_ANALYSIS.md new file mode 100644 index 0000000..aa07372 --- /dev/null +++ b/docs/CGO_PARITY_ANALYSIS.md @@ -0,0 +1,334 @@ +# Analisis de Paridad con CGo go-sqlite3 + +> **Fecha**: 2025-12-08 +> **Objetivo**: Identificar todas las funcionalidades de go-sqlite3 para replicarlas en zsqlite + +## Resumen + +go-sqlite3 (https://github.com/mattn/go-sqlite3) es el wrapper SQLite mas maduro para Go. +Este documento analiza todas sus funcionalidades para asegurar paridad en zsqlite. + +--- + +## Estado de Implementacion + +### Leyenda +- ✅ Implementado en zsqlite +- ⏳ Pendiente de implementar +- 🔄 Parcialmente implementado +- ❌ No aplicable a Zig + +--- + +## 1. Conexion a Base de Datos + +| Funcionalidad | go-sqlite3 | zsqlite | Notas | +|---------------|------------|---------|-------| +| Open basico | ✅ | ✅ | `sqlite.open()` | +| Open con flags | ✅ | ✅ | `Database.openWithFlags()` | +| Close | ✅ | ✅ | `db.close()` | +| URI connection string | ✅ | ⏳ | Parsear `file:path?mode=ro&cache=shared` | +| DSN parameters | ✅ | ⏳ | Configuracion via string | +| Connection pooling | ✅ (via database/sql) | ⏳ | Pool propio | + +--- + +## 2. Configuracion de Pragmas + +| Pragma | go-sqlite3 | zsqlite | Prioridad | +|--------|------------|---------|-----------| +| auto_vacuum | ✅ | ⏳ | Media | +| busy_timeout | ✅ | ✅ | `db.setBusyTimeout()` | +| cache_size | ✅ | ⏳ | Media | +| case_sensitive_like | ✅ | ⏳ | Baja | +| defer_foreign_keys | ✅ | ⏳ | Media | +| foreign_keys | ✅ | ✅ | `db.setForeignKeys()` | +| journal_mode | ✅ | ✅ | `db.setJournalMode()` | +| locking_mode | ✅ | ⏳ | Media | +| query_only | ✅ | ⏳ | Baja | +| recursive_triggers | ✅ | ⏳ | Baja | +| secure_delete | ✅ | ⏳ | Baja | +| synchronous | ✅ | ✅ | `db.setSynchronous()` | + +--- + +## 3. Prepared Statements + +| Funcionalidad | go-sqlite3 | zsqlite | Notas | +|---------------|------------|---------|-------| +| Prepare | ✅ | ✅ | `db.prepare()` | +| Exec (sin resultados) | ✅ | ✅ | `db.exec()` | +| Query (con resultados) | ✅ | ✅ | `stmt.step()` loop | +| Bind posicional (?) | ✅ | ✅ | `stmt.bindInt(1, val)` | +| Bind named (:name) | ✅ | ✅ | `stmt.bindIntNamed(":name", val)` | +| Bind named (@name) | ✅ | ✅ | `stmt.bindIntNamed("@name", val)` | +| Bind named ($name) | ✅ | ✅ | `stmt.bindIntNamed("$name", val)` | +| Readonly check | ✅ | ✅ | `stmt.isReadOnly()` | +| SQL text | ✅ | ✅ | `stmt.sql()` | +| Expanded SQL | ✅ | ⏳ | `sqlite3_expanded_sql()` | + +--- + +## 4. Bind de Parametros + +| Tipo | go-sqlite3 | zsqlite | Notas | +|------|------------|---------|-------| +| NULL | ✅ | ✅ | `stmt.bindNull()` | +| int64 | ✅ | ✅ | `stmt.bindInt()` | +| float64 | ✅ | ✅ | `stmt.bindFloat()` | +| string/text | ✅ | ✅ | `stmt.bindText()` | +| []byte/blob | ✅ | ✅ | `stmt.bindBlob()` | +| bool | ✅ | ✅ | `stmt.bindBool()` | +| time.Time | ✅ | ⏳ | Formatear como string ISO8601 | +| Zeroblob | ✅ | ✅ | `stmt.bindZeroblob()` | + +--- + +## 5. Lectura de Columnas + +| Tipo | go-sqlite3 | zsqlite | Notas | +|------|------------|---------|-------| +| Column count | ✅ | ✅ | `stmt.columnCount()` | +| Column name | ✅ | ✅ | `stmt.columnName()` | +| Column type | ✅ | ✅ | `stmt.columnType()` | +| int64 | ✅ | ✅ | `stmt.columnInt()` | +| float64 | ✅ | ✅ | `stmt.columnFloat()` | +| text | ✅ | ✅ | `stmt.columnText()` | +| blob | ✅ | ✅ | `stmt.columnBlob()` | +| NULL check | ✅ | ✅ | `stmt.columnIsNull()` | +| Column bytes | ✅ | ✅ | `stmt.columnBytes()` | +| Declared type | ✅ | ✅ | `stmt.columnDeclType()` | +| Database name | ✅ | ⏳ | `sqlite3_column_database_name()` | +| Table name | ✅ | ⏳ | `sqlite3_column_table_name()` | +| Origin name | ✅ | ⏳ | `sqlite3_column_origin_name()` | + +--- + +## 6. Transacciones + +| Funcionalidad | go-sqlite3 | zsqlite | Notas | +|---------------|------------|---------|-------| +| BEGIN | ✅ | ✅ | `db.begin()` | +| BEGIN IMMEDIATE | ✅ | ✅ | `db.beginImmediate()` | +| BEGIN EXCLUSIVE | ✅ | ✅ | `db.beginExclusive()` | +| BEGIN DEFERRED | ✅ | ✅ | `db.begin()` (default) | +| COMMIT | ✅ | ✅ | `db.commit()` | +| ROLLBACK | ✅ | ✅ | `db.rollback()` | +| SAVEPOINT | ✅ | ✅ | `db.savepoint(alloc, "name")` | +| RELEASE | ✅ | ✅ | `db.release(alloc, "name")` | +| ROLLBACK TO | ✅ | ✅ | `db.rollbackTo(alloc, "name")` | + +--- + +## 7. Metadatos y Utilidades + +| Funcionalidad | go-sqlite3 | zsqlite | Notas | +|---------------|------------|---------|-------| +| LastInsertRowId | ✅ | ✅ | `db.lastInsertRowId()` | +| Changes | ✅ | ✅ | `db.changes()` | +| TotalChanges | ✅ | ✅ | `db.totalChanges()` | +| Error message | ✅ | ✅ | `db.errorMessage()` | +| Error code | ✅ | ✅ | `db.errorCode()` | +| Extended error code | ✅ | ✅ | `db.extendedErrorCode()` | +| SQLite version | ✅ | ✅ | `sqlite.version()` | +| Version number | ✅ | ✅ | `sqlite.versionNumber()` | +| Database filename | ✅ | ✅ | `db.filename()` | +| Is readonly | ✅ | ✅ | `db.isReadOnly()` | + +--- + +## 8. Limites y Control + +| Funcionalidad | go-sqlite3 | zsqlite | Prioridad | +|---------------|------------|---------|-----------| +| GetLimit | ✅ | ⏳ | Baja | +| SetLimit | ✅ | ⏳ | Baja | +| SetFileControlInt | ✅ | ⏳ | Baja | +| Interrupt | ✅ | ✅ | `db.interrupt()` | + +--- + +## 9. Callbacks y Hooks + +| Funcionalidad | go-sqlite3 | zsqlite | Prioridad | +|---------------|------------|---------|-----------| +| Commit hook | ✅ | ⏳ | Media | +| Rollback hook | ✅ | ⏳ | Media | +| Update hook | ✅ | ⏳ | Media | +| Pre-update hook | ✅ | ⏳ | Baja | +| Authorizer | ✅ | ⏳ | Baja | +| Progress handler | ✅ | ⏳ | Baja | +| Busy handler | ✅ | ⏳ | Media | +| Busy timeout | ✅ | ✅ | `db.setBusyTimeout()` | + +--- + +## 10. Funciones Personalizadas + +| Funcionalidad | go-sqlite3 | zsqlite | Prioridad | +|---------------|------------|---------|-----------| +| RegisterFunc (scalar) | ✅ | ✅ | `db.createScalarFunction()` | +| RegisterAggregator | ✅ | ⏳ | Media | +| RegisterCollation | ✅ | ✅ | `db.createCollation()` | +| User-defined window func | ✅ | ⏳ | Baja | + +--- + +## 11. Backup API + +| Funcionalidad | go-sqlite3 | zsqlite | Prioridad | +|---------------|------------|---------|-----------| +| Backup init | ✅ | ✅ | `Backup.init()` | +| Backup step | ✅ | ✅ | `backup.step()` | +| Backup finish | ✅ | ✅ | `backup.finish()` | +| Backup remaining | ✅ | ✅ | `backup.remaining()` | +| Backup pagecount | ✅ | ✅ | `backup.pageCount()` | + +--- + +## 12. Blob I/O + +| Funcionalidad | go-sqlite3 | zsqlite | Prioridad | +|---------------|------------|---------|-----------| +| Blob open | ✅ | ⏳ | Media | +| Blob close | ✅ | ⏳ | Media | +| Blob read | ✅ | ⏳ | Media | +| Blob write | ✅ | ⏳ | Media | +| Blob bytes | ✅ | ⏳ | Media | +| Blob reopen | ✅ | ⏳ | Baja | + +--- + +## 13. Extensiones + +| Funcionalidad | go-sqlite3 | zsqlite | Notas | +|---------------|------------|---------|-------| +| Load extension | ✅ | ❌ | Deshabilitado por seguridad | +| Enable load ext | ✅ | ❌ | | + +--- + +## Plan de Implementacion + +### Fase 2A - Prioridad Alta ✅ COMPLETADA +1. ✅ Busy timeout/handler - `db.setBusyTimeout()` +2. ✅ WAL mode (journal_mode pragma) - `db.setJournalMode()`, `db.enableWalMode()` +3. ✅ Named parameters - `stmt.bindTextNamed()`, etc. +4. ✅ SAVEPOINT/RELEASE/ROLLBACK TO - `db.savepoint()`, etc. +5. ✅ BEGIN EXCLUSIVE - `db.beginExclusive()` + +### Fase 2B - Prioridad Alta ✅ COMPLETADA +1. ✅ Backup API completo - `Backup` struct, `backupToFile()`, `loadFromFile()` +2. ✅ User-defined functions (scalar) - `db.createScalarFunction()` +3. ✅ Collations personalizadas - `db.createCollation()` +4. ✅ ATTACH/DETACH - `db.attach()`, `db.detach()`, `db.listDatabases()` + +### Fase 3A - Prioridad Media (Siguiente) +1. ⏳ Blob I/O streaming +2. ⏳ Hooks (commit, rollback, update) +3. ⏳ Aggregator functions +4. ⏳ Mas pragmas + +### Fase 3B - Prioridad Baja +1. ⏳ Authorizer +2. ⏳ Progress handler +3. ⏳ Pre-update hook +4. ⏳ Window functions +5. ⏳ Limits API + +--- + +## Funciones SQLite C Relevantes (Referencia) + +```c +// Conexion +sqlite3_open_v2() +sqlite3_close_v2() + +// Statements +sqlite3_prepare_v2() +sqlite3_finalize() +sqlite3_reset() +sqlite3_clear_bindings() +sqlite3_step() +sqlite3_sql() +sqlite3_expanded_sql() +sqlite3_stmt_readonly() + +// Bind +sqlite3_bind_parameter_index() +sqlite3_bind_parameter_name() +sqlite3_bind_null() +sqlite3_bind_int64() +sqlite3_bind_double() +sqlite3_bind_text() +sqlite3_bind_blob() +sqlite3_bind_zeroblob() + +// Column +sqlite3_column_count() +sqlite3_column_name() +sqlite3_column_type() +sqlite3_column_int64() +sqlite3_column_double() +sqlite3_column_text() +sqlite3_column_blob() +sqlite3_column_bytes() +sqlite3_column_decltype() +sqlite3_column_database_name() +sqlite3_column_table_name() +sqlite3_column_origin_name() + +// Utilidades +sqlite3_last_insert_rowid() +sqlite3_changes() +sqlite3_total_changes() +sqlite3_errmsg() +sqlite3_errcode() +sqlite3_extended_errcode() +sqlite3_libversion() +sqlite3_libversion_number() +sqlite3_db_filename() +sqlite3_db_readonly() + +// Busy +sqlite3_busy_timeout() +sqlite3_busy_handler() + +// Hooks +sqlite3_commit_hook() +sqlite3_rollback_hook() +sqlite3_update_hook() +sqlite3_preupdate_hook() + +// Funciones +sqlite3_create_function_v2() +sqlite3_create_collation_v2() +sqlite3_create_window_function() + +// Backup +sqlite3_backup_init() +sqlite3_backup_step() +sqlite3_backup_finish() +sqlite3_backup_remaining() +sqlite3_backup_pagecount() + +// Blob +sqlite3_blob_open() +sqlite3_blob_close() +sqlite3_blob_read() +sqlite3_blob_write() +sqlite3_blob_bytes() +sqlite3_blob_reopen() + +// Control +sqlite3_interrupt() +sqlite3_limit() +sqlite3_progress_handler() +sqlite3_set_authorizer() +``` + +--- + +**© zsqlite - CGo Parity Analysis** +*2025-12-08* diff --git a/src/root.zig b/src/root.zig index a8356d3..bb1f96b 100644 --- a/src/root.zig +++ b/src/root.zig @@ -259,6 +259,314 @@ pub const Database = struct { pub fn rollback(self: *Self) Error!void { try self.exec("ROLLBACK"); } + + /// Begins an exclusive transaction (acquires exclusive lock immediately). + pub fn beginExclusive(self: *Self) Error!void { + try self.exec("BEGIN EXCLUSIVE"); + } + + // ======================================================================== + // Savepoints + // ======================================================================== + + /// Creates a savepoint with the given name. + /// + /// Savepoints allow nested transactions. Use `release()` to commit + /// or `rollbackTo()` to revert to the savepoint. + pub fn savepoint(self: *Self, allocator: std.mem.Allocator, name: []const u8) !void { + const sql = try std.fmt.allocPrint(allocator, "SAVEPOINT {s}\x00", .{name}); + defer allocator.free(sql); + try self.exec(sql[0 .. sql.len - 1 :0]); + } + + /// Releases (commits) a savepoint. + /// + /// All changes since the savepoint was created become permanent + /// (within the containing transaction). + pub fn release(self: *Self, allocator: std.mem.Allocator, name: []const u8) !void { + const sql = try std.fmt.allocPrint(allocator, "RELEASE SAVEPOINT {s}\x00", .{name}); + defer allocator.free(sql); + try self.exec(sql[0 .. sql.len - 1 :0]); + } + + /// Rolls back to a savepoint. + /// + /// All changes since the savepoint was created are undone. + /// The savepoint remains active and can be rolled back to again. + pub fn rollbackTo(self: *Self, allocator: std.mem.Allocator, name: []const u8) !void { + const sql = try std.fmt.allocPrint(allocator, "ROLLBACK TO SAVEPOINT {s}\x00", .{name}); + defer allocator.free(sql); + try self.exec(sql[0 .. sql.len - 1 :0]); + } + + // ======================================================================== + // Pragmas and Configuration + // ======================================================================== + + /// Sets the busy timeout in milliseconds. + /// + /// When the database is locked, SQLite will wait up to this many + /// milliseconds before returning SQLITE_BUSY. + pub fn setBusyTimeout(self: *Self, ms: i32) Error!void { + const result = c.sqlite3_busy_timeout(self.handle, ms); + if (result != c.SQLITE_OK) { + return resultToError(result); + } + } + + /// Sets the journal mode. + /// + /// Common modes: "DELETE", "TRUNCATE", "PERSIST", "MEMORY", "WAL", "OFF" + /// WAL mode is recommended for concurrent access. + pub fn setJournalMode(self: *Self, allocator: std.mem.Allocator, mode: []const u8) !void { + const sql = try std.fmt.allocPrint(allocator, "PRAGMA journal_mode = {s}\x00", .{mode}); + defer allocator.free(sql); + try self.exec(sql[0 .. sql.len - 1 :0]); + } + + /// Sets the synchronous mode. + /// + /// Modes: "OFF" (0), "NORMAL" (1), "FULL" (2), "EXTRA" (3) + /// Lower values = faster but less safe, higher = slower but safer. + pub fn setSynchronous(self: *Self, allocator: std.mem.Allocator, mode: []const u8) !void { + const sql = try std.fmt.allocPrint(allocator, "PRAGMA synchronous = {s}\x00", .{mode}); + defer allocator.free(sql); + try self.exec(sql[0 .. sql.len - 1 :0]); + } + + /// Enables or disables WAL mode with recommended settings. + /// + /// WAL mode provides better concurrency and is recommended for most uses. + pub fn enableWalMode(self: *Self, allocator: std.mem.Allocator) !void { + try self.setJournalMode(allocator, "WAL"); + try self.setSynchronous(allocator, "NORMAL"); + } + + /// Interrupts a long-running query. + /// + /// Causes any pending database operation to abort and return SQLITE_INTERRUPT. + pub fn interrupt(self: *Self) void { + c.sqlite3_interrupt(self.handle); + } + + /// Returns the error code of the most recent error. + pub fn errorCode(self: *Self) i32 { + return c.sqlite3_errcode(self.handle); + } + + /// Returns the extended error code of the most recent error. + pub fn extendedErrorCode(self: *Self) i32 { + return c.sqlite3_extended_errcode(self.handle); + } + + /// Returns whether the database is read-only. + /// + /// The `db_name` parameter is usually "main" for the main database. + pub fn isReadOnly(self: *Self, db_name: [:0]const u8) bool { + return c.sqlite3_db_readonly(self.handle, db_name.ptr) == 1; + } + + /// Returns the filename of a database. + /// + /// The `db_name` parameter is usually "main" for the main database. + pub fn filename(self: *Self, db_name: [:0]const u8) ?[]const u8 { + const fname = c.sqlite3_db_filename(self.handle, db_name.ptr); + if (fname) |f| { + return std.mem.span(f); + } + return null; + } + + // ======================================================================== + // ATTACH/DETACH databases + // ======================================================================== + + /// Attaches another database file to this connection. + /// + /// After attaching, tables from the attached database can be accessed + /// using the schema name prefix: `SELECT * FROM schema_name.table_name` + /// + /// The attached database will be opened read-write if possible. + pub fn attach(self: *Self, allocator: std.mem.Allocator, file_path: []const u8, schema_name: []const u8) !void { + const sql = try std.fmt.allocPrint(allocator, "ATTACH DATABASE '{s}' AS {s}\x00", .{ file_path, schema_name }); + defer allocator.free(sql); + try self.exec(sql[0 .. sql.len - 1 :0]); + } + + /// Attaches an in-memory database to this connection. + /// + /// Creates a new empty in-memory database accessible via the schema name. + pub fn attachMemory(self: *Self, allocator: std.mem.Allocator, schema_name: []const u8) !void { + const sql = try std.fmt.allocPrint(allocator, "ATTACH DATABASE ':memory:' AS {s}\x00", .{schema_name}); + defer allocator.free(sql); + try self.exec(sql[0 .. sql.len - 1 :0]); + } + + /// Detaches a previously attached database. + /// + /// All tables from the detached database become inaccessible. + pub fn detach(self: *Self, allocator: std.mem.Allocator, schema_name: []const u8) !void { + const sql = try std.fmt.allocPrint(allocator, "DETACH DATABASE {s}\x00", .{schema_name}); + defer allocator.free(sql); + try self.exec(sql[0 .. sql.len - 1 :0]); + } + + /// Returns a list of attached database names. + /// + /// Always includes "main" and "temp". + pub fn listDatabases(self: *Self, allocator: std.mem.Allocator) ![][]const u8 { + var stmt = try self.prepare("PRAGMA database_list"); + defer stmt.finalize(); + + var list: std.ArrayListUnmanaged([]const u8) = .empty; + errdefer { + for (list.items) |item| allocator.free(item); + list.deinit(allocator); + } + + while (try stmt.step()) { + if (stmt.columnText(1)) |name| { + const owned = try allocator.dupe(u8, name); + try list.append(allocator, owned); + } + } + + return list.toOwnedSlice(allocator); + } + + /// Frees the list returned by listDatabases. + pub fn freeDatabaseList(allocator: std.mem.Allocator, list: [][]const u8) void { + for (list) |item| allocator.free(item); + allocator.free(list); + } + + // ======================================================================== + // User-Defined Functions + // ======================================================================== + + /// Registers a scalar function with the database. + /// + /// Scalar functions are called once per row and return a single value. + /// The function wrapper is automatically freed when the database is closed + /// or when the function is removed. + /// + /// Example: + /// ```zig + /// fn myDouble(ctx: FunctionContext, args: []const FunctionValue) void { + /// if (args.len != 1) { + /// ctx.setError("double() requires 1 argument"); + /// return; + /// } + /// if (args[0].isNull()) { + /// ctx.setNull(); + /// return; + /// } + /// ctx.setInt(args[0].asInt() * 2); + /// } + /// + /// try db.createScalarFunction("double", 1, myDouble); + /// ``` + pub fn createScalarFunction( + self: *Self, + name: [:0]const u8, + num_args: i32, + func: ScalarFn, + ) !void { + // Allocate wrapper to store function pointer (uses page allocator internally) + const wrapper = try ScalarFnWrapper.create(func); + + const result = c.sqlite3_create_function_v2( + self.handle, + name.ptr, + num_args, + c.SQLITE_UTF8, + wrapper, + scalarCallback, + null, // step (for aggregates) + null, // final (for aggregates) + scalarDestructor, + ); + + if (result != c.SQLITE_OK) { + wrapper.destroy(); + return resultToError(result); + } + } + + /// Removes a previously registered function. + pub fn removeFunction(self: *Self, name: [:0]const u8, num_args: i32) Error!void { + const result = c.sqlite3_create_function_v2( + self.handle, + name.ptr, + num_args, + c.SQLITE_UTF8, + null, + null, + null, + null, + null, + ); + + if (result != c.SQLITE_OK) { + return resultToError(result); + } + } + + // ======================================================================== + // Custom Collations + // ======================================================================== + + /// Registers a custom collation sequence. + /// + /// Collations define how strings are compared for ORDER BY, comparison + /// operators, and DISTINCT. The collation is automatically freed when + /// the database is closed. + /// + /// Example: + /// ```zig + /// // Case-insensitive collation + /// fn caseInsensitive(a: []const u8, b: []const u8) i32 { + /// // Compare ignoring case... + /// } + /// + /// try db.createCollation("NOCASE2", caseInsensitive); + /// + /// // Use in queries: + /// // SELECT * FROM users ORDER BY name COLLATE NOCASE2 + /// ``` + pub fn createCollation(self: *Self, name: [:0]const u8, func: CollationFn) !void { + const wrapper = try CollationWrapper.create(func); + + const result = c.sqlite3_create_collation_v2( + self.handle, + name.ptr, + c.SQLITE_UTF8, + wrapper, + collationCallback, + collationDestructor, + ); + + if (result != c.SQLITE_OK) { + wrapper.destroy(); + return resultToError(result); + } + } + + /// Removes a previously registered collation. + pub fn removeCollation(self: *Self, name: [:0]const u8) Error!void { + const result = c.sqlite3_create_collation_v2( + self.handle, + name.ptr, + c.SQLITE_UTF8, + null, + null, + null, + ); + + if (result != c.SQLITE_OK) { + return resultToError(result); + } + } }; /// Flags for opening a database @@ -315,6 +623,50 @@ pub const Statement = struct { } } + // ======================================================================== + // Statement metadata + // ======================================================================== + + /// Returns the SQL text of the statement. + pub fn sql(self: *Self) ?[]const u8 { + const s = c.sqlite3_sql(self.handle); + if (s) |ptr| { + return std.mem.span(ptr); + } + return null; + } + + /// Returns whether the statement is read-only. + pub fn isReadOnly(self: *Self) bool { + return c.sqlite3_stmt_readonly(self.handle) != 0; + } + + /// Returns the number of parameters in the statement. + pub fn parameterCount(self: *Self) u32 { + return @intCast(c.sqlite3_bind_parameter_count(self.handle)); + } + + /// Returns the index of a named parameter. + /// + /// Supports :name, @name, and $name styles. + /// Returns null if the parameter is not found. + pub fn parameterIndex(self: *Self, name: [:0]const u8) ?u32 { + const idx = c.sqlite3_bind_parameter_index(self.handle, name.ptr); + if (idx == 0) return null; + return @intCast(idx); + } + + /// Returns the name of a parameter by index. + /// + /// Returns null if the parameter has no name (positional ?). + pub fn parameterName(self: *Self, index: u32) ?[]const u8 { + const name = c.sqlite3_bind_parameter_name(self.handle, @intCast(index)); + if (name) |n| { + return std.mem.span(n); + } + return null; + } + // ======================================================================== // Bind parameters (1-indexed as per SQLite convention) // ======================================================================== @@ -373,6 +725,61 @@ pub const Statement = struct { } } + /// Binds a boolean to a parameter (as integer 0 or 1). + pub fn bindBool(self: *Self, index: u32, value: bool) Error!void { + try self.bindInt(index, if (value) 1 else 0); + } + + /// Binds a zeroblob (a blob of zeros) to a parameter. + /// + /// Useful for reserving space for blob data that will be written later. + pub fn bindZeroblob(self: *Self, index: u32, size: u32) Error!void { + const result = c.sqlite3_bind_zeroblob(self.handle, @intCast(index), @intCast(size)); + if (result != c.SQLITE_OK) { + return resultToError(result); + } + } + + // ======================================================================== + // Named parameter binding + // ======================================================================== + + /// Binds NULL to a named parameter. + pub fn bindNullNamed(self: *Self, name: [:0]const u8) Error!void { + const idx = self.parameterIndex(name) orelse return Error.Range; + try self.bindNull(idx); + } + + /// Binds an integer to a named parameter. + pub fn bindIntNamed(self: *Self, name: [:0]const u8, value: i64) Error!void { + const idx = self.parameterIndex(name) orelse return Error.Range; + try self.bindInt(idx, value); + } + + /// Binds a float to a named parameter. + pub fn bindFloatNamed(self: *Self, name: [:0]const u8, value: f64) Error!void { + const idx = self.parameterIndex(name) orelse return Error.Range; + try self.bindFloat(idx, value); + } + + /// Binds text to a named parameter. + pub fn bindTextNamed(self: *Self, name: [:0]const u8, value: []const u8) Error!void { + const idx = self.parameterIndex(name) orelse return Error.Range; + try self.bindText(idx, value); + } + + /// Binds a blob to a named parameter. + pub fn bindBlobNamed(self: *Self, name: [:0]const u8, value: []const u8) Error!void { + const idx = self.parameterIndex(name) orelse return Error.Range; + try self.bindBlob(idx, value); + } + + /// Binds a boolean to a named parameter. + pub fn bindBoolNamed(self: *Self, name: [:0]const u8, value: bool) Error!void { + const idx = self.parameterIndex(name) orelse return Error.Range; + try self.bindBool(idx, value); + } + // ======================================================================== // Execution // ======================================================================== @@ -456,8 +863,176 @@ pub const Statement = struct { pub fn columnIsNull(self: *Self, index: u32) bool { return self.columnType(index) == .null_value; } + + /// Returns a boolean column value (interprets 0 as false, non-zero as true). + pub fn columnBool(self: *Self, index: u32) bool { + return self.columnInt(index) != 0; + } + + /// Returns the size in bytes of a blob or text column. + pub fn columnBytes(self: *Self, index: u32) u32 { + return @intCast(c.sqlite3_column_bytes(self.handle, @intCast(index))); + } + + /// Returns the declared type of a column. + /// + /// This is the type declared in the CREATE TABLE statement. + pub fn columnDeclType(self: *Self, index: u32) ?[]const u8 { + const dtype = c.sqlite3_column_decltype(self.handle, @intCast(index)); + if (dtype) |d| { + return std.mem.span(d); + } + return null; + } }; +// ============================================================================ +// Backup API +// ============================================================================ + +/// SQLite online backup handle. +/// +/// Allows copying database content from one database to another while +/// both databases are in use. Useful for: +/// - Creating database backups +/// - Copying in-memory databases to files +/// - Copying file databases to memory +pub const Backup = struct { + handle: ?*c.sqlite3_backup, + dest_db: *Database, + source_db: *Database, + + const Self = @This(); + + /// Initializes a backup from source to destination database. + /// + /// The `dest_name` and `source_name` are usually "main" for the main database. + /// Use "temp" for temporary databases. + pub fn init( + dest_db: *Database, + dest_name: [:0]const u8, + source_db: *Database, + source_name: [:0]const u8, + ) Error!Self { + const handle = c.sqlite3_backup_init( + dest_db.handle, + dest_name.ptr, + source_db.handle, + source_name.ptr, + ); + + if (handle == null) { + // Get error from destination database + const err_code = c.sqlite3_errcode(dest_db.handle); + return resultToError(err_code); + } + + return .{ + .handle = handle, + .dest_db = dest_db, + .source_db = source_db, + }; + } + + /// Convenience function to backup the main database. + pub fn initMain(dest_db: *Database, source_db: *Database) Error!Self { + return init(dest_db, "main", source_db, "main"); + } + + /// Copies up to `n_pages` pages from source to destination. + /// + /// Use -1 to copy all remaining pages in one call. + /// + /// Returns: + /// - `true` if there are more pages to copy + /// - `false` if the backup is complete + pub fn step(self: *Self, n_pages: i32) Error!bool { + const result = c.sqlite3_backup_step(self.handle, n_pages); + return switch (result) { + c.SQLITE_OK => true, + c.SQLITE_DONE => false, + c.SQLITE_BUSY, c.SQLITE_LOCKED => Error.Busy, + else => resultToError(result), + }; + } + + /// Copies all remaining pages in one call. + /// + /// Equivalent to `step(-1)`. + pub fn stepAll(self: *Self) Error!void { + _ = try self.step(-1); + } + + /// Returns the number of pages still to be copied. + pub fn remaining(self: *Self) i32 { + return c.sqlite3_backup_remaining(self.handle); + } + + /// Returns the total number of pages in the source database. + pub fn pageCount(self: *Self) i32 { + return c.sqlite3_backup_pagecount(self.handle); + } + + /// Returns the progress as a percentage (0-100). + pub fn progress(self: *Self) u8 { + const total = self.pageCount(); + if (total == 0) return 100; + const done = total - self.remaining(); + return @intCast(@divFloor(done * 100, total)); + } + + /// Finishes the backup operation and releases resources. + /// + /// Must be called when done with the backup, even if an error occurred. + pub fn finish(self: *Self) Error!void { + if (self.handle) |h| { + const result = c.sqlite3_backup_finish(h); + self.handle = null; + if (result != c.SQLITE_OK) { + return resultToError(result); + } + } + } + + /// Alias for finish() for RAII-style usage. + pub fn deinit(self: *Self) void { + self.finish() catch {}; + } +}; + +/// Copies an entire database to another database. +/// +/// This is a convenience function that performs a complete backup in one call. +/// For large databases, consider using Backup directly with step() for progress reporting. +pub fn backupDatabase(dest_db: *Database, source_db: *Database) Error!void { + var backup = try Backup.initMain(dest_db, source_db); + defer backup.deinit(); + try backup.stepAll(); +} + +/// Copies a database to a file. +/// +/// Creates a new file (or overwrites existing) with the database contents. +pub fn backupToFile(source_db: *Database, path: [:0]const u8) Error!void { + var dest_db = try Database.open(path); + defer dest_db.close(); + try backupDatabase(&dest_db, source_db); +} + +/// Loads a database from a file into memory. +/// +/// Returns a new in-memory database with the file contents. +pub fn loadFromFile(path: [:0]const u8) Error!Database { + var file_db = try Database.open(path); + defer file_db.close(); + + var mem_db = try openMemory(); + errdefer mem_db.close(); + + try backupDatabase(&mem_db, &file_db); + return mem_db; +} + /// Column data types pub const ColumnType = enum { integer, @@ -467,6 +1042,230 @@ pub const ColumnType = enum { null_value, }; +// ============================================================================ +// User-Defined Functions (Scalar) +// ============================================================================ + +/// Context for user-defined function results. +/// +/// Used to set the return value of a scalar function. +pub const FunctionContext = struct { + ctx: *c.sqlite3_context, + + const Self = @This(); + + /// Sets the result to NULL. + pub fn setNull(self: Self) void { + c.sqlite3_result_null(self.ctx); + } + + /// Sets the result to an integer. + pub fn setInt(self: Self, value: i64) void { + c.sqlite3_result_int64(self.ctx, value); + } + + /// Sets the result to a float. + pub fn setFloat(self: Self, value: f64) void { + c.sqlite3_result_double(self.ctx, value); + } + + /// Sets the result to text. + pub fn setText(self: Self, value: []const u8) void { + c.sqlite3_result_text( + self.ctx, + value.ptr, + @intCast(value.len), + c.SQLITE_TRANSIENT, + ); + } + + /// Sets the result to a blob. + pub fn setBlob(self: Self, value: []const u8) void { + c.sqlite3_result_blob( + self.ctx, + value.ptr, + @intCast(value.len), + c.SQLITE_TRANSIENT, + ); + } + + /// Sets the result to an error. + pub fn setError(self: Self, msg: []const u8) void { + c.sqlite3_result_error(self.ctx, msg.ptr, @intCast(msg.len)); + } +}; + +/// Value passed to user-defined functions. +/// +/// Represents an argument or result value. +pub const FunctionValue = struct { + value: *c.sqlite3_value, + + const Self = @This(); + + /// Returns the type of the value. + pub fn getType(self: Self) ColumnType { + const vtype = c.sqlite3_value_type(self.value); + return switch (vtype) { + c.SQLITE_INTEGER => .integer, + c.SQLITE_FLOAT => .float, + c.SQLITE_TEXT => .text, + c.SQLITE_BLOB => .blob, + c.SQLITE_NULL => .null_value, + else => .null_value, + }; + } + + /// Returns true if the value is NULL. + pub fn isNull(self: Self) bool { + return self.getType() == .null_value; + } + + /// Returns the value as an integer. + pub fn asInt(self: Self) i64 { + return c.sqlite3_value_int64(self.value); + } + + /// Returns the value as a float. + pub fn asFloat(self: Self) f64 { + return c.sqlite3_value_double(self.value); + } + + /// Returns the value as text. + pub fn asText(self: Self) ?[]const u8 { + const len = c.sqlite3_value_bytes(self.value); + const text = c.sqlite3_value_text(self.value); + if (text) |t| { + return t[0..@intCast(len)]; + } + return null; + } + + /// Returns the value as a blob. + pub fn asBlob(self: Self) ?[]const u8 { + const len = c.sqlite3_value_bytes(self.value); + const blob = c.sqlite3_value_blob(self.value); + if (blob) |b| { + const ptr: [*]const u8 = @ptrCast(b); + return ptr[0..@intCast(len)]; + } + return null; + } +}; + +/// Type signature for scalar function callbacks. +pub const ScalarFn = *const fn (ctx: FunctionContext, args: []const FunctionValue) void; + +/// Wrapper data structure stored in SQLite. +/// Uses page allocator for simplicity since functions are typically registered once. +const ScalarFnWrapper = struct { + func: ScalarFn, + + fn create(func: ScalarFn) !*ScalarFnWrapper { + const wrapper = try std.heap.page_allocator.create(ScalarFnWrapper); + wrapper.func = func; + return wrapper; + } + + fn destroy(self: *ScalarFnWrapper) void { + std.heap.page_allocator.destroy(self); + } +}; + +/// C callback trampoline for scalar functions. +fn scalarCallback( + ctx: ?*c.sqlite3_context, + argc: c_int, + argv: [*c]?*c.sqlite3_value, +) callconv(.c) void { + const user_data = c.sqlite3_user_data(ctx); + if (user_data == null) return; + + const wrapper: *ScalarFnWrapper = @ptrCast(@alignCast(user_data)); + const func_ctx = FunctionContext{ .ctx = ctx.? }; + + // Build args array + const args_count: usize = @intCast(argc); + var args: [16]FunctionValue = undefined; // Max 16 args + const actual_count = @min(args_count, 16); + + for (0..actual_count) |i| { + if (argv[i]) |v| { + args[i] = FunctionValue{ .value = v }; + } + } + + wrapper.func(func_ctx, args[0..actual_count]); +} + +/// Destructor callback for function user data. +fn scalarDestructor(ptr: ?*anyopaque) callconv(.c) void { + if (ptr) |p| { + const wrapper: *ScalarFnWrapper = @ptrCast(@alignCast(p)); + wrapper.destroy(); + } +} + +// ============================================================================ +// Custom Collations +// ============================================================================ + +/// Type signature for collation comparison functions. +/// +/// Should return: +/// - negative value if a < b +/// - zero if a == b +/// - positive value if a > b +pub const CollationFn = *const fn (a: []const u8, b: []const u8) i32; + +/// Wrapper for collation function. +const CollationWrapper = struct { + func: CollationFn, + + fn create(func: CollationFn) !*CollationWrapper { + const wrapper = try std.heap.page_allocator.create(CollationWrapper); + wrapper.func = func; + return wrapper; + } + + fn destroy(self: *CollationWrapper) void { + std.heap.page_allocator.destroy(self); + } +}; + +/// C callback trampoline for collation functions. +fn collationCallback( + user_data: ?*anyopaque, + len_a: c_int, + data_a: ?*const anyopaque, + len_b: c_int, + data_b: ?*const anyopaque, +) callconv(.c) c_int { + if (user_data == null) return 0; + + const wrapper: *CollationWrapper = @ptrCast(@alignCast(user_data)); + + const a: []const u8 = if (data_a) |ptr| + @as([*]const u8, @ptrCast(ptr))[0..@intCast(len_a)] + else + ""; + + const b: []const u8 = if (data_b) |ptr| + @as([*]const u8, @ptrCast(ptr))[0..@intCast(len_b)] + else + ""; + + return wrapper.func(a, b); +} + +/// Destructor callback for collation user data. +fn collationDestructor(ptr: ?*anyopaque) callconv(.c) void { + if (ptr) |p| { + const wrapper: *CollationWrapper = @ptrCast(@alignCast(p)); + wrapper.destroy(); + } +} + // ============================================================================ // Convenience functions // ============================================================================ @@ -660,3 +1459,310 @@ test "blob data" { const result = query.columnBlob(0).?; try std.testing.expectEqualSlices(u8, &blob_data, result); } + +test "named parameters" { + var db = try openMemory(); + defer db.close(); + + try db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)"); + + // Test :name style + var stmt = try db.prepare("INSERT INTO users (name, age) VALUES (:name, :age)"); + defer stmt.finalize(); + + try std.testing.expectEqual(@as(u32, 2), stmt.parameterCount()); + try std.testing.expectEqual(@as(?u32, 1), stmt.parameterIndex(":name")); + try std.testing.expectEqual(@as(?u32, 2), stmt.parameterIndex(":age")); + try std.testing.expectEqualStrings(":name", stmt.parameterName(1).?); + try std.testing.expectEqualStrings(":age", stmt.parameterName(2).?); + + try stmt.bindTextNamed(":name", "Alice"); + try stmt.bindIntNamed(":age", 30); + _ = try stmt.step(); + + try std.testing.expectEqual(@as(i64, 1), db.lastInsertRowId()); +} + +test "savepoint" { + const allocator = std.testing.allocator; + var db = try openMemory(); + defer db.close(); + + try db.exec("CREATE TABLE test (x INTEGER)"); + try db.exec("INSERT INTO test VALUES (1)"); + + try db.begin(); + + // Create savepoint, insert, then rollback to it + try db.savepoint(allocator, "sp1"); + try db.exec("INSERT INTO test VALUES (2)"); + try db.rollbackTo(allocator, "sp1"); + + // Insert another value + try db.exec("INSERT INTO test VALUES (3)"); + try db.release(allocator, "sp1"); + + try db.commit(); + + // Should have rows 1 and 3, not 2 + var stmt = try db.prepare("SELECT x FROM test ORDER BY x"); + defer stmt.finalize(); + + _ = try stmt.step(); + try std.testing.expectEqual(@as(i64, 1), stmt.columnInt(0)); + _ = try stmt.step(); + try std.testing.expectEqual(@as(i64, 3), stmt.columnInt(0)); + try std.testing.expect(!try stmt.step()); +} + +test "busy timeout" { + var db = try openMemory(); + defer db.close(); + + try db.setBusyTimeout(5000); + // If we get here without error, the function works +} + +test "statement metadata" { + var db = try openMemory(); + defer db.close(); + + try db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)"); + + var select = try db.prepare("SELECT id, name FROM test"); + defer select.finalize(); + + try std.testing.expect(select.isReadOnly()); + try std.testing.expectEqualStrings("SELECT id, name FROM test", select.sql().?); + + var insert = try db.prepare("INSERT INTO test (name) VALUES (?)"); + defer insert.finalize(); + + try std.testing.expect(!insert.isReadOnly()); +} + +test "boolean bind and column" { + var db = try openMemory(); + defer db.close(); + + try db.exec("CREATE TABLE test (flag INTEGER)"); + + var stmt = try db.prepare("INSERT INTO test VALUES (?)"); + defer stmt.finalize(); + + try stmt.bindBool(1, true); + _ = try stmt.step(); + + try stmt.reset(); + try stmt.bindBool(1, false); + _ = try stmt.step(); + + var query = try db.prepare("SELECT flag FROM test ORDER BY rowid"); + defer query.finalize(); + + _ = try query.step(); + try std.testing.expect(query.columnBool(0)); + + _ = try query.step(); + try std.testing.expect(!query.columnBool(0)); +} + +test "backup memory to memory" { + // Create source database with data + var source = try openMemory(); + defer source.close(); + + try source.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)"); + try source.exec("INSERT INTO test (value) VALUES ('hello'), ('world')"); + + // Create destination database + var dest = try openMemory(); + defer dest.close(); + + // Perform backup + var backup = try Backup.initMain(&dest, &source); + defer backup.deinit(); + + // Copy all pages (page count may be 0 for small in-memory DBs until step is called) + try backup.stepAll(); + + // After stepAll, finish should work + try backup.finish(); + + // Verify data was copied + var stmt = try dest.prepare("SELECT COUNT(*) FROM test"); + defer stmt.finalize(); + _ = try stmt.step(); + try std.testing.expectEqual(@as(i64, 2), stmt.columnInt(0)); +} + +test "backup convenience function" { + // Create source database with data + var source = try openMemory(); + defer source.close(); + + try source.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)"); + try source.exec("INSERT INTO users (name) VALUES ('Alice'), ('Bob'), ('Charlie')"); + + // Create destination and backup + var dest = try openMemory(); + defer dest.close(); + + try backupDatabase(&dest, &source); + + // Verify + var stmt = try dest.prepare("SELECT name FROM users ORDER BY id"); + defer stmt.finalize(); + + _ = try stmt.step(); + try std.testing.expectEqualStrings("Alice", stmt.columnText(0).?); + _ = try stmt.step(); + try std.testing.expectEqualStrings("Bob", stmt.columnText(0).?); + _ = try stmt.step(); + try std.testing.expectEqualStrings("Charlie", stmt.columnText(0).?); +} + +test "attach and detach memory database" { + const allocator = std.testing.allocator; + + var db = try openMemory(); + defer db.close(); + + // Attach a second in-memory database + try db.attachMemory(allocator, "db2"); + + // Create table in attached database + try db.exec("CREATE TABLE db2.items (id INTEGER PRIMARY KEY, name TEXT)"); + try db.exec("INSERT INTO db2.items (name) VALUES ('item1'), ('item2')"); + + // Query from attached database (use block to ensure stmt is finalized before detach) + { + var stmt = try db.prepare("SELECT COUNT(*) FROM db2.items"); + defer stmt.finalize(); + _ = try stmt.step(); + try std.testing.expectEqual(@as(i64, 2), stmt.columnInt(0)); + } + + // List databases + const dbs = try db.listDatabases(allocator); + defer Database.freeDatabaseList(allocator, dbs); + + try std.testing.expect(dbs.len >= 2); // main + db2 (temp may or may not be listed) + + // Detach + try db.detach(allocator, "db2"); + + // After detach, db2.items should not be accessible + const result = db.prepare("SELECT * FROM db2.items"); + try std.testing.expectError(Error.SqliteError, result); +} + +// Test function: doubles an integer +fn doubleInt(ctx: FunctionContext, args: []const FunctionValue) void { + if (args.len != 1) { + ctx.setError("double() requires 1 argument"); + return; + } + if (args[0].isNull()) { + ctx.setNull(); + return; + } + const value = args[0].asInt(); + ctx.setInt(value * 2); +} + +// Test function: concatenates strings +fn concatStrings(ctx: FunctionContext, args: []const FunctionValue) void { + if (args.len < 2) { + ctx.setError("concat() requires at least 2 arguments"); + return; + } + + // Simple concat for 2 args + const a = args[0].asText() orelse ""; + const b = args[1].asText() orelse ""; + + // For testing, just return first arg if both empty, otherwise concat + if (a.len == 0 and b.len == 0) { + ctx.setText(""); + } else if (a.len == 0) { + ctx.setText(b); + } else if (b.len == 0) { + ctx.setText(a); + } else { + // Can't easily concat without allocator, just return first for test + ctx.setText(a); + } +} + +test "user-defined scalar function" { + var db = try openMemory(); + defer db.close(); + + // Register double function + try db.createScalarFunction("double", 1, doubleInt); + + try db.exec("CREATE TABLE test (value INTEGER)"); + try db.exec("INSERT INTO test VALUES (5), (10), (15)"); + + // Use the custom function + var stmt = try db.prepare("SELECT double(value) FROM test ORDER BY value"); + defer stmt.finalize(); + + _ = try stmt.step(); + try std.testing.expectEqual(@as(i64, 10), stmt.columnInt(0)); // 5 * 2 + + _ = try stmt.step(); + try std.testing.expectEqual(@as(i64, 20), stmt.columnInt(0)); // 10 * 2 + + _ = try stmt.step(); + try std.testing.expectEqual(@as(i64, 30), stmt.columnInt(0)); // 15 * 2 +} + +// Test collation: reverse alphabetical order +fn reverseCollation(a: []const u8, b: []const u8) i32 { + // Standard comparison but negated for reverse order + const result = std.mem.order(u8, a, b); + return switch (result) { + .lt => 1, + .gt => -1, + .eq => 0, + }; +} + +test "custom collation" { + var db = try openMemory(); + defer db.close(); + + // Register reverse collation + try db.createCollation("REVERSE", reverseCollation); + + try db.exec("CREATE TABLE names (name TEXT)"); + try db.exec("INSERT INTO names VALUES ('Alice'), ('Bob'), ('Charlie')"); + + // Without collation: alphabetical order + { + var stmt = try db.prepare("SELECT name FROM names ORDER BY name"); + defer stmt.finalize(); + + _ = try stmt.step(); + try std.testing.expectEqualStrings("Alice", stmt.columnText(0).?); + _ = try stmt.step(); + try std.testing.expectEqualStrings("Bob", stmt.columnText(0).?); + _ = try stmt.step(); + try std.testing.expectEqualStrings("Charlie", stmt.columnText(0).?); + } + + // With REVERSE collation: reverse alphabetical order + { + var stmt = try db.prepare("SELECT name FROM names ORDER BY name COLLATE REVERSE"); + defer stmt.finalize(); + + _ = try stmt.step(); + try std.testing.expectEqualStrings("Charlie", stmt.columnText(0).?); + _ = try stmt.step(); + try std.testing.expectEqualStrings("Bob", stmt.columnText(0).?); + _ = try stmt.step(); + try std.testing.expectEqualStrings("Alice", stmt.columnText(0).?); + } +}