From 7229c27c8057ff24d8342acffa0621a778fd933c Mon Sep 17 00:00:00 2001 From: reugenio Date: Mon, 8 Dec 2025 19:33:46 +0100 Subject: [PATCH] Fase 4: Window functions, URI, pragmas y connection pool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Window functions: createWindowFunction() con 4 callbacks (xStep, xFinal, xValue, xInverse) - URI connection string: openUri() y openUriAlloc() (file:path?mode=ro&cache=shared) - Pragmas adicionales: setAutoVacuum, setCacheSize, setCaseSensitiveLike, setDeferForeignKeys, setLockingMode, setQueryOnly, setRecursiveTriggers, setSecureDelete, setPageSize, setMaxPageCount, setTempStore, setWalAutoCheckpoint - Maintenance: vacuum, incrementalVacuum, optimize, integrityCheck, quickCheck, walCheckpoint - ConnectionPool: pool thread-safe con acquire/release Paridad 100% con go-sqlite3 (excepto extensiones dinamicas que estan deshabilitadas por seguridad) πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- docs/API.md | 198 ++++++++- docs/ARCHITECTURE.md | 6 +- docs/CGO_PARITY_ANALYSIS.md | 35 +- src/root.zig | 774 ++++++++++++++++++++++++++++++++++++ 4 files changed, 994 insertions(+), 19 deletions(-) diff --git a/docs/API.md b/docs/API.md index 0062128..c052112 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1,6 +1,6 @@ # zsqlite - API Reference -> **Version**: 0.5 +> **Version**: 0.6 > **Ultima actualizacion**: 2025-12-08 ## Quick Reference @@ -11,8 +11,15 @@ const sqlite = @import("zsqlite"); // Abrir base de datos var db = try sqlite.openMemory(); // In-memory var db = try sqlite.open("file.db"); // Archivo +var db = try sqlite.openUri("file:test.db?mode=ro"); // URI defer db.close(); +// Connection pool +var pool = try sqlite.ConnectionPool.init(alloc, "file:db?cache=shared", 10); +defer pool.deinit(); +const conn = try pool.acquire(); +defer pool.release(conn); + // SQL directo try db.exec("CREATE TABLE ..."); @@ -46,6 +53,7 @@ var restored = try sqlite.loadFromFile("backup.db"); // User-defined functions try db.createScalarFunction("double", 1, myDoubleFunc); try db.createAggregateFunction("sum_squares", 1, stepFn, finalFn); +try db.createWindowFunction("running_sum", 1, stepFn, finalFn, valueFn, inverseFn); // Custom collations try db.createCollation("NOCASE2", myCaseInsensitiveCompare); @@ -70,6 +78,16 @@ try stmt.bindCurrentTime(1); // Limits const old = db.setLimit(.sql_length, 10000); const current = db.getLimit(.sql_length); + +// Pragmas adicionales +try db.setAutoVacuum(alloc, "full"); +try db.setCacheSize(alloc, 10000); +try db.setLockingMode(alloc, "exclusive"); + +// Maintenance +try db.vacuum(); +try db.optimize(); +const result = try db.integrityCheck(alloc); ``` --- @@ -150,6 +168,8 @@ Carga una base de datos desde archivo a memoria. |---------|-------------| | `open(path)` | Abre conexion (read-write, create) | | `openWithFlags(path, flags)` | Abre con flags especificos | +| `openUri(uri)` | Abre con URI (file:path?mode=ro&cache=shared) | +| `openUriAlloc(alloc, uri)` | openUri con string runtime | | `close()` | Cierra la conexion | ### Ejecucion SQL @@ -200,6 +220,29 @@ try db.commit(); | `setJournalMode(alloc, mode)` | "WAL", "DELETE", etc | | `setSynchronous(alloc, mode)` | "OFF", "NORMAL", "FULL" | | `enableWalMode(alloc)` | WAL + NORMAL sync | +| `setAutoVacuum(alloc, mode)` | "none", "full", "incremental" | +| `setCacheSize(alloc, size)` | Tamano de cache en KB | +| `setCaseSensitiveLike(enabled)` | LIKE case-sensitive | +| `setDeferForeignKeys(enabled)` | Diferir FK checks | +| `setLockingMode(alloc, mode)` | "normal", "exclusive" | +| `setQueryOnly(enabled)` | Solo lectura | +| `setRecursiveTriggers(enabled)` | Triggers recursivos | +| `setSecureDelete(enabled)` | Borrado seguro | +| `setPageSize(alloc, size)` | Tamano de pagina | +| `setMaxPageCount(alloc, count)` | Max paginas | +| `setTempStore(alloc, mode)` | "default", "file", "memory" | +| `setWalAutoCheckpoint(alloc, n)` | Checkpoint cada N paginas | + +### Maintenance + +| Funcion | Descripcion | +|---------|-------------| +| `vacuum()` | Compacta la base de datos | +| `incrementalVacuum(alloc, pages)` | Vacuum incremental | +| `optimize()` | Optimiza indices | +| `integrityCheck(alloc)` | Verifica integridad | +| `quickCheck(alloc)` | Verificacion rapida | +| `walCheckpoint(alloc, mode)` | Checkpoint WAL | ### ATTACH/DETACH @@ -935,5 +978,156 @@ try stmt.bindCurrentTime(1); --- -**Β© zsqlite v0.5 - API Reference** +## Window Functions + +Funciones de ventana personalizadas para queries analiticos. + +### Types + +```zig +pub const WindowValueFn = *const fn (ctx: AggregateContext) void; +pub const WindowInverseFn = *const fn (ctx: AggregateContext, args: []const FunctionValue) void; +``` + +### Database Method + +```zig +pub fn createWindowFunction( + self: *Database, + name: [:0]const u8, + num_args: i32, + step_fn: AggregateStepFn, // Agrega valor a ventana + final_fn: AggregateFinalFn, // Retorna resultado final + value_fn: WindowValueFn, // Retorna valor actual + inverse_fn: WindowInverseFn, // Remueve valor de ventana +) !void +``` + +### Ejemplo: Running Sum + +```zig +const SumState = struct { total: i64 = 0 }; + +fn runningSumStep(ctx: AggregateContext, args: []const FunctionValue) void { + const state = ctx.getAggregateContext(SumState) orelse return; + if (args.len > 0 and !args[0].isNull()) { + state.total += args[0].asInt(); + } +} + +fn runningSumFinal(ctx: AggregateContext) void { + const state = ctx.getAggregateContext(SumState) orelse { + ctx.setNull(); + return; + }; + ctx.setInt(state.total); +} + +fn runningSumValue(ctx: AggregateContext) void { + // Igual que final para running sum + runningSumFinal(ctx); +} + +fn runningSumInverse(ctx: AggregateContext, args: []const FunctionValue) void { + const state = ctx.getAggregateContext(SumState) orelse return; + if (args.len > 0 and !args[0].isNull()) { + state.total -= args[0].asInt(); + } +} + +try db.createWindowFunction("running_sum", 1, + runningSumStep, runningSumFinal, runningSumValue, runningSumInverse); + +// SELECT running_sum(value) OVER (ORDER BY id ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) +// FROM numbers +``` + +--- + +## Connection Pool + +Pool de conexiones para aplicaciones multi-hilo. + +### Struct + +```zig +pub const ConnectionPool = struct { + pub fn init(allocator: Allocator, path: [:0]const u8, max_size: usize) !ConnectionPool + pub fn deinit(self: *ConnectionPool) void + pub fn acquire(self: *ConnectionPool) !*Database + pub fn release(self: *ConnectionPool, conn: *Database) void + pub fn inUseCount(self: *ConnectionPool) usize + pub fn openCount(self: *ConnectionPool) usize + pub fn capacity(self: *ConnectionPool) usize +}; +``` + +### Ejemplo + +```zig +var pool = try sqlite.ConnectionPool.init(allocator, "file:app.db?cache=shared", 10); +defer pool.deinit(); + +// En cada thread/request: +const conn = try pool.acquire(); +defer pool.release(conn); + +try conn.exec("INSERT INTO logs (msg) VALUES ('Hello')"); +``` + +### Notas + +- El pool usa mutex para thread-safety +- Las conexiones se crean bajo demanda (lazy) +- Las conexiones liberadas se reutilizan +- Usar URI con `cache=shared` para compartir cache entre conexiones + +--- + +## URI Connection String + +Conexion via URI con parametros. + +### Funciones + +```zig +pub fn openUri(uri: [:0]const u8) Error!Database +pub fn openUriAlloc(allocator: Allocator, uri: []const u8) !Database +``` + +### Sintaxis URI + +``` +file:path?param1=value1¶m2=value2 +``` + +### Parametros Soportados + +| Parametro | Valores | Descripcion | +|-----------|---------|-------------| +| `mode` | ro, rw, rwc, memory | Modo de apertura | +| `cache` | shared, private | Cache compartida | +| `psow` | 0, 1 | Power-safe overwrite | +| `nolock` | 1 | Sin locking | +| `immutable` | 1 | Base inmutable | + +### Ejemplos + +```zig +// Solo lectura +var db = try sqlite.openUri("file:data.db?mode=ro"); + +// Memoria compartida (para pools) +var db = try sqlite.openUri("file::memory:?cache=shared"); + +// Read-write, crear si no existe +var db = try sqlite.openUri("file:app.db?mode=rwc"); + +// Base inmutable (sin journal) +var db = try sqlite.openUri("file:static.db?immutable=1"); +``` + +--- + +**Β© zsqlite v0.6 - API Reference** *2025-12-08* diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 88667c1..72f8680 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,6 +1,6 @@ # zsqlite - Arquitectura Tecnica -> **Version**: 0.3 +> **Version**: 0.6 > **Ultima actualizacion**: 2025-12-08 ## Vision General @@ -13,8 +13,8 @@ zsqlite es un wrapper de SQLite para Zig que compila SQLite amalgamation directa β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ zsqlite API β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ -β”‚ β”‚ Database β”‚ β”‚ Statement β”‚ β”‚ Error β”‚ β”‚ Column β”‚ β”‚ -β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ Mapping β”‚ β”‚ Type β”‚ β”‚ +β”‚ β”‚ Database β”‚ β”‚ Statement β”‚ β”‚ Backup β”‚ β”‚ConnPool β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ Blob β”‚ β”‚ Functions β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ diff --git a/docs/CGO_PARITY_ANALYSIS.md b/docs/CGO_PARITY_ANALYSIS.md index 900403a..fa3db8a 100644 --- a/docs/CGO_PARITY_ANALYSIS.md +++ b/docs/CGO_PARITY_ANALYSIS.md @@ -27,9 +27,9 @@ Este documento analiza todas sus funcionalidades para asegurar paridad en zsqlit | 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 | +| URI connection string | βœ… | βœ… | `db.openUri()` soporta `file:path?mode=ro&cache=shared` | +| DSN parameters | βœ… | βœ… | Via URI connection string | +| Connection pooling | βœ… (via database/sql) | βœ… | `ConnectionPool` struct | --- @@ -37,17 +37,17 @@ Este documento analiza todas sus funcionalidades para asegurar paridad en zsqlit | Pragma | go-sqlite3 | zsqlite | Prioridad | |--------|------------|---------|-----------| -| auto_vacuum | βœ… | ⏳ | Media | +| auto_vacuum | βœ… | βœ… | `db.setAutoVacuum()` | | busy_timeout | βœ… | βœ… | `db.setBusyTimeout()` | -| cache_size | βœ… | ⏳ | Media | -| case_sensitive_like | βœ… | ⏳ | Baja | -| defer_foreign_keys | βœ… | ⏳ | Media | +| cache_size | βœ… | βœ… | `db.setCacheSize()` | +| case_sensitive_like | βœ… | βœ… | `db.setCaseSensitiveLike()` | +| defer_foreign_keys | βœ… | βœ… | `db.setDeferForeignKeys()` | | foreign_keys | βœ… | βœ… | `db.setForeignKeys()` | | journal_mode | βœ… | βœ… | `db.setJournalMode()` | -| locking_mode | βœ… | ⏳ | Media | -| query_only | βœ… | ⏳ | Baja | -| recursive_triggers | βœ… | ⏳ | Baja | -| secure_delete | βœ… | ⏳ | Baja | +| locking_mode | βœ… | βœ… | `db.setLockingMode()` | +| query_only | βœ… | βœ… | `db.setQueryOnly()` | +| recursive_triggers | βœ… | βœ… | `db.setRecursiveTriggers()` | +| secure_delete | βœ… | βœ… | `db.setSecureDelete()` | | synchronous | βœ… | βœ… | `db.setSynchronous()` | --- @@ -170,7 +170,7 @@ Este documento analiza todas sus funcionalidades para asegurar paridad en zsqlit | RegisterFunc (scalar) | βœ… | βœ… | `db.createScalarFunction()` | | RegisterAggregator | βœ… | βœ… | `db.createAggregateFunction()` | | RegisterCollation | βœ… | βœ… | `db.createCollation()` | -| User-defined window func | βœ… | ⏳ | Baja | +| User-defined window func | βœ… | βœ… | `db.createWindowFunction()` | --- @@ -227,7 +227,7 @@ Este documento analiza todas sus funcionalidades para asegurar paridad en zsqlit 1. βœ… Blob I/O streaming - `Blob` struct con `read()`, `write()`, `reopen()` 2. βœ… Hooks (commit, rollback, update) - `db.setCommitHook()`, etc. 3. βœ… Aggregator functions - `db.createAggregateFunction()` -4. ⏳ Mas pragmas +4. βœ… Mas pragmas ### Fase 3B - Prioridad Baja βœ… COMPLETADA 1. βœ… Authorizer - `db.setAuthorizer()` @@ -238,7 +238,14 @@ Este documento analiza todas sus funcionalidades para asegurar paridad en zsqlit 6. βœ… Timestamp binding - `stmt.bindTimestamp()` 7. βœ… Column metadata - `stmt.columnDatabaseName()`, etc. 8. βœ… Expanded SQL - `stmt.expandedSql()` -9. ⏳ Window functions (baja prioridad) +9. βœ… Window functions - `db.createWindowFunction()` + +### Fase 4 - Paridad Completa βœ… COMPLETADA +1. βœ… Window functions - `db.createWindowFunction()` con xStep, xFinal, xValue, xInverse +2. βœ… URI connection string - `db.openUri()`, `sqlite.openUri()` +3. βœ… Pragmas adicionales - `setAutoVacuum`, `setCacheSize`, `setLockingMode`, etc. +4. βœ… Connection pooling - `ConnectionPool` struct con acquire/release +5. βœ… Maintenance - `vacuum()`, `optimize()`, `integrityCheck()`, `walCheckpoint()` --- diff --git a/src/root.zig b/src/root.zig index be888c9..b79f165 100644 --- a/src/root.zig +++ b/src/root.zig @@ -169,6 +169,44 @@ pub const Database = struct { return .{ .handle = handle }; } + /// Opens a database using a URI connection string. + /// + /// Supports SQLite URI format: + /// - `file:path/to/db.sqlite` - Regular file + /// - `file::memory:` - In-memory database + /// - `file:path?mode=ro` - Read-only + /// - `file:path?mode=rw` - Read-write + /// - `file:path?mode=rwc` - Read-write, create if not exists + /// - `file:path?cache=shared` - Shared cache + /// - `file:path?cache=private` - Private cache + /// + /// Example: + /// ```zig + /// var db = try Database.openUri("file:test.db?mode=ro"); + /// defer db.close(); + /// ``` + pub fn openUri(uri: [:0]const u8) Error!Self { + var handle: ?*c.sqlite3 = null; + const flags = c.SQLITE_OPEN_URI | c.SQLITE_OPEN_READWRITE | c.SQLITE_OPEN_CREATE; + const result = c.sqlite3_open_v2(uri.ptr, &handle, flags, null); + + if (result != c.SQLITE_OK) { + if (handle) |h| { + _ = c.sqlite3_close(h); + } + return resultToError(result); + } + + return .{ .handle = handle }; + } + + /// Opens a database using a URI with an allocator for runtime strings. + pub fn openUriAlloc(allocator: std.mem.Allocator, uri: []const u8) !Self { + const uri_z = try allocator.dupeZ(u8, uri); + defer allocator.free(uri_z); + return Self.openUri(uri_z); + } + /// Closes the database connection. pub fn close(self: *Self) void { if (self.handle) |h| { @@ -345,6 +383,163 @@ pub const Database = struct { try self.setSynchronous(allocator, "NORMAL"); } + /// Sets the auto_vacuum mode. + /// + /// Modes: "NONE" (0), "FULL" (1), "INCREMENTAL" (2) + /// Note: Can only be changed when database is empty. + pub fn setAutoVacuum(self: *Self, allocator: std.mem.Allocator, mode: []const u8) !void { + const sql = try std.fmt.allocPrint(allocator, "PRAGMA auto_vacuum = {s}\x00", .{mode}); + defer allocator.free(sql); + try self.exec(sql[0 .. sql.len - 1 :0]); + } + + /// Sets the cache size (in pages, or negative for kilobytes). + /// + /// Example: -2000 for 2MB cache, 1000 for 1000 pages + pub fn setCacheSize(self: *Self, allocator: std.mem.Allocator, size: i32) !void { + const sql = try std.fmt.allocPrint(allocator, "PRAGMA cache_size = {d}\x00", .{size}); + defer allocator.free(sql); + try self.exec(sql[0 .. sql.len - 1 :0]); + } + + /// Enables or disables case-sensitive LIKE. + pub fn setCaseSensitiveLike(self: *Self, enabled: bool) Error!void { + const sql: [:0]const u8 = if (enabled) "PRAGMA case_sensitive_like = ON" else "PRAGMA case_sensitive_like = OFF"; + try self.exec(sql); + } + + /// Enables or disables deferred foreign key enforcement. + /// + /// When enabled, foreign key constraints are not checked until COMMIT. + pub fn setDeferForeignKeys(self: *Self, enabled: bool) Error!void { + const sql: [:0]const u8 = if (enabled) "PRAGMA defer_foreign_keys = ON" else "PRAGMA defer_foreign_keys = OFF"; + try self.exec(sql); + } + + /// Sets the locking mode. + /// + /// Modes: "NORMAL", "EXCLUSIVE" + /// EXCLUSIVE mode prevents other connections from accessing the database. + pub fn setLockingMode(self: *Self, allocator: std.mem.Allocator, mode: []const u8) !void { + const sql = try std.fmt.allocPrint(allocator, "PRAGMA locking_mode = {s}\x00", .{mode}); + defer allocator.free(sql); + try self.exec(sql[0 .. sql.len - 1 :0]); + } + + /// Enables or disables query_only mode. + /// + /// When enabled, prevents any changes to the database. + pub fn setQueryOnly(self: *Self, enabled: bool) Error!void { + const sql: [:0]const u8 = if (enabled) "PRAGMA query_only = ON" else "PRAGMA query_only = OFF"; + try self.exec(sql); + } + + /// Enables or disables recursive triggers. + pub fn setRecursiveTriggers(self: *Self, enabled: bool) Error!void { + const sql: [:0]const u8 = if (enabled) "PRAGMA recursive_triggers = ON" else "PRAGMA recursive_triggers = OFF"; + try self.exec(sql); + } + + /// Enables or disables secure delete. + /// + /// When enabled, deleted content is overwritten with zeros. + pub fn setSecureDelete(self: *Self, enabled: bool) Error!void { + const sql: [:0]const u8 = if (enabled) "PRAGMA secure_delete = ON" else "PRAGMA secure_delete = OFF"; + try self.exec(sql); + } + + /// Sets the page size (must be power of 2 between 512 and 65536). + /// + /// Note: Can only be changed when database is empty. + pub fn setPageSize(self: *Self, allocator: std.mem.Allocator, size: u32) !void { + const sql = try std.fmt.allocPrint(allocator, "PRAGMA page_size = {d}\x00", .{size}); + defer allocator.free(sql); + try self.exec(sql[0 .. sql.len - 1 :0]); + } + + /// Sets the maximum page count. + pub fn setMaxPageCount(self: *Self, allocator: std.mem.Allocator, count: u32) !void { + const sql = try std.fmt.allocPrint(allocator, "PRAGMA max_page_count = {d}\x00", .{count}); + defer allocator.free(sql); + try self.exec(sql[0 .. sql.len - 1 :0]); + } + + /// Sets the temp_store location. + /// + /// Modes: "DEFAULT" (0), "FILE" (1), "MEMORY" (2) + pub fn setTempStore(self: *Self, allocator: std.mem.Allocator, mode: []const u8) !void { + const sql = try std.fmt.allocPrint(allocator, "PRAGMA temp_store = {s}\x00", .{mode}); + defer allocator.free(sql); + try self.exec(sql[0 .. sql.len - 1 :0]); + } + + /// Sets the WAL auto-checkpoint interval (in pages). + /// + /// 0 disables auto-checkpoint. Default is 1000 pages. + pub fn setWalAutoCheckpoint(self: *Self, allocator: std.mem.Allocator, pages: u32) !void { + const sql = try std.fmt.allocPrint(allocator, "PRAGMA wal_autocheckpoint = {d}\x00", .{pages}); + defer allocator.free(sql); + try self.exec(sql[0 .. sql.len - 1 :0]); + } + + /// Runs integrity check and returns result. + /// + /// Returns "ok" if no problems found. + pub fn integrityCheck(self: *Self, allocator: std.mem.Allocator) ![]u8 { + var stmt = try self.prepare("PRAGMA integrity_check"); + defer stmt.finalize(); + + if (try stmt.step()) { + if (stmt.columnText(0)) |text| { + return try allocator.dupe(u8, text); + } + } + return try allocator.dupe(u8, "unknown"); + } + + /// Runs quick integrity check (faster, less thorough). + pub fn quickCheck(self: *Self, allocator: std.mem.Allocator) ![]u8 { + var stmt = try self.prepare("PRAGMA quick_check"); + defer stmt.finalize(); + + if (try stmt.step()) { + if (stmt.columnText(0)) |text| { + return try allocator.dupe(u8, text); + } + } + return try allocator.dupe(u8, "unknown"); + } + + /// Runs VACUUM to rebuild the database file. + pub fn vacuum(self: *Self) Error!void { + try self.exec("VACUUM"); + } + + /// Runs incremental vacuum (only with auto_vacuum = INCREMENTAL). + pub fn incrementalVacuum(self: *Self, allocator: std.mem.Allocator, pages: ?u32) !void { + if (pages) |p| { + const sql = try std.fmt.allocPrint(allocator, "PRAGMA incremental_vacuum({d})\x00", .{p}); + defer allocator.free(sql); + try self.exec(sql[0 .. sql.len - 1 :0]); + } else { + try self.exec("PRAGMA incremental_vacuum"); + } + } + + /// Performs a WAL checkpoint. + /// + /// Modes: "PASSIVE", "FULL", "RESTART", "TRUNCATE" + pub fn walCheckpoint(self: *Self, allocator: std.mem.Allocator, mode: []const u8) !void { + const sql = try std.fmt.allocPrint(allocator, "PRAGMA wal_checkpoint({s})\x00", .{mode}); + defer allocator.free(sql); + try self.exec(sql[0 .. sql.len - 1 :0]); + } + + /// Optimizes the database (analyzes tables that need it). + pub fn optimize(self: *Self) Error!void { + try self.exec("PRAGMA optimize"); + } + /// Interrupts a long-running query. /// /// Causes any pending database operation to abort and return SQLITE_INTERRUPT. @@ -574,6 +769,76 @@ pub const Database = struct { } } + /// Creates a user-defined window function. + /// + /// Window functions are similar to aggregate functions but can also be used + /// with OVER clauses to compute values across a set of rows related to the + /// current row. + /// + /// Parameters: + /// - `name`: Name of the function in SQL + /// - `num_args`: Number of arguments (-1 for variadic) + /// - `step_fn`: Called for each row to accumulate values + /// - `final_fn`: Called at the end to produce the final result + /// - `value_fn`: Called to get the current value during window processing + /// - `inverse_fn`: Called to remove a row from the window (inverse of step) + /// + /// Example: + /// ```zig + /// const SumState = struct { total: i64 = 0 }; + /// + /// fn sumStep(ctx: AggregateContext, args: []const FunctionValue) void { + /// const state = ctx.getAggregateContext(SumState) orelse return; + /// if (!args[0].isNull()) state.total += args[0].asInt(); + /// } + /// + /// fn sumInverse(ctx: AggregateContext, args: []const FunctionValue) void { + /// const state = ctx.getAggregateContext(SumState) orelse return; + /// if (!args[0].isNull()) state.total -= args[0].asInt(); + /// } + /// + /// fn sumValue(ctx: AggregateContext) void { + /// const state = ctx.getAggregateContext(SumState) orelse { ctx.setNull(); return; }; + /// ctx.setInt(state.total); + /// } + /// + /// fn sumFinal(ctx: AggregateContext) void { + /// sumValue(ctx); // Same as value for this simple case + /// } + /// + /// try db.createWindowFunction("mysum", 1, sumStep, sumFinal, sumValue, sumInverse); + /// // SELECT mysum(value) OVER (ORDER BY id ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) + /// ``` + pub fn createWindowFunction( + self: *Self, + name: [:0]const u8, + num_args: i32, + step_fn: AggregateStepFn, + final_fn: AggregateFinalFn, + value_fn: WindowValueFn, + inverse_fn: WindowInverseFn, + ) !void { + const wrapper = try WindowFnWrapper.create(step_fn, final_fn, value_fn, inverse_fn); + + const result = c.sqlite3_create_window_function( + self.handle, + name.ptr, + num_args, + c.SQLITE_UTF8, + wrapper, + windowStepCallback, + windowFinalCallback, + windowValueCallback, + windowInverseCallback, + windowDestructor, + ); + + if (result != c.SQLITE_OK) { + wrapper.destroy(); + return resultToError(result); + } + } + // ======================================================================== // Custom Collations // ======================================================================== @@ -1749,6 +2014,126 @@ fn aggregateDestructor(ptr: ?*anyopaque) callconv(.c) void { } } +// ============================================================================ +// Window Functions +// ============================================================================ + +/// Type signature for window function xValue callback. +/// Called to return the current value of the aggregate window. +/// Unlike xFinal, this is called while the window is still being processed. +pub const WindowValueFn = *const fn (ctx: AggregateContext) void; + +/// Type signature for window function xInverse callback. +/// Called to remove the oldest row from the current window. +/// This is the inverse of xStep - if xStep adds, xInverse subtracts. +pub const WindowInverseFn = *const fn (ctx: AggregateContext, args: []const FunctionValue) void; + +/// Wrapper for window function callbacks. +const WindowFnWrapper = struct { + step_fn: AggregateStepFn, + final_fn: AggregateFinalFn, + value_fn: WindowValueFn, + inverse_fn: WindowInverseFn, + + fn create( + step_fn: AggregateStepFn, + final_fn: AggregateFinalFn, + value_fn: WindowValueFn, + inverse_fn: WindowInverseFn, + ) !*WindowFnWrapper { + const wrapper = try std.heap.page_allocator.create(WindowFnWrapper); + wrapper.step_fn = step_fn; + wrapper.final_fn = final_fn; + wrapper.value_fn = value_fn; + wrapper.inverse_fn = inverse_fn; + return wrapper; + } + + fn destroy(self: *WindowFnWrapper) void { + std.heap.page_allocator.destroy(self); + } +}; + +/// C callback trampoline for window step function. +fn windowStepCallback( + 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: *WindowFnWrapper = @ptrCast(@alignCast(user_data)); + const agg_ctx = AggregateContext{ .ctx = ctx.? }; + + const args_count: usize = @intCast(argc); + var args: [16]FunctionValue = undefined; + const actual_count = @min(args_count, 16); + + for (0..actual_count) |i| { + if (argv[i]) |v| { + args[i] = FunctionValue{ .value = v }; + } + } + + wrapper.step_fn(agg_ctx, args[0..actual_count]); +} + +/// C callback trampoline for window final function. +fn windowFinalCallback(ctx: ?*c.sqlite3_context) callconv(.c) void { + const user_data = c.sqlite3_user_data(ctx); + if (user_data == null) return; + + const wrapper: *WindowFnWrapper = @ptrCast(@alignCast(user_data)); + const agg_ctx = AggregateContext{ .ctx = ctx.? }; + + wrapper.final_fn(agg_ctx); +} + +/// C callback trampoline for window value function. +fn windowValueCallback(ctx: ?*c.sqlite3_context) callconv(.c) void { + const user_data = c.sqlite3_user_data(ctx); + if (user_data == null) return; + + const wrapper: *WindowFnWrapper = @ptrCast(@alignCast(user_data)); + const agg_ctx = AggregateContext{ .ctx = ctx.? }; + + wrapper.value_fn(agg_ctx); +} + +/// C callback trampoline for window inverse function. +fn windowInverseCallback( + 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: *WindowFnWrapper = @ptrCast(@alignCast(user_data)); + const agg_ctx = AggregateContext{ .ctx = ctx.? }; + + const args_count: usize = @intCast(argc); + var args: [16]FunctionValue = undefined; + const actual_count = @min(args_count, 16); + + for (0..actual_count) |i| { + if (argv[i]) |v| { + args[i] = FunctionValue{ .value = v }; + } + } + + wrapper.inverse_fn(agg_ctx, args[0..actual_count]); +} + +/// Destructor callback for window function user data. +fn windowDestructor(ptr: ?*anyopaque) callconv(.c) void { + if (ptr) |p| { + const wrapper: *WindowFnWrapper = @ptrCast(@alignCast(p)); + wrapper.destroy(); + } +} + // ============================================================================ // Custom Collations // ============================================================================ @@ -2398,6 +2783,15 @@ pub fn openMemory() Error!Database { return Database.open(":memory:"); } +/// Opens a database using a URI connection string. +/// +/// Example URIs: +/// - `file:test.db?mode=ro` - Read-only +/// - `file::memory:?cache=shared` - Shared memory database +pub fn openUri(uri: [:0]const u8) Error!Database { + return Database.openUri(uri); +} + /// Returns the SQLite version string. pub fn version() []const u8 { return std.mem.span(c.sqlite3_libversion()); @@ -2408,6 +2802,154 @@ pub fn versionNumber() i32 { return c.sqlite3_libversion_number(); } +// ============================================================================ +// Connection Pool +// ============================================================================ + +/// A simple connection pool for SQLite databases. +/// +/// Manages a pool of database connections that can be acquired and released +/// for concurrent access. All connections share the same database file. +/// +/// Example: +/// ```zig +/// var pool = try ConnectionPool.init(allocator, "mydb.sqlite", 4); +/// defer pool.deinit(); +/// +/// var conn = try pool.acquire(); +/// defer pool.release(conn); +/// +/// try conn.exec("SELECT ..."); +/// ``` +pub const ConnectionPool = struct { + allocator: std.mem.Allocator, + path: []u8, + connections: []?Database, + in_use: []bool, + mutex: std.Thread.Mutex, + max_size: usize, + + const Self = @This(); + + /// Creates a new connection pool. + /// + /// Parameters: + /// - `allocator`: Allocator for pool management + /// - `path`: Database file path + /// - `max_size`: Maximum number of connections in the pool + pub fn init(allocator: std.mem.Allocator, path: [:0]const u8, max_size: usize) !Self { + const path_copy = try allocator.dupe(u8, path); + errdefer allocator.free(path_copy); + + const connections = try allocator.alloc(?Database, max_size); + errdefer allocator.free(connections); + @memset(connections, null); + + const in_use = try allocator.alloc(bool, max_size); + errdefer allocator.free(in_use); + @memset(in_use, false); + + return .{ + .allocator = allocator, + .path = path_copy, + .connections = connections, + .in_use = in_use, + .mutex = .{}, + .max_size = max_size, + }; + } + + /// Destroys the connection pool, closing all connections. + pub fn deinit(self: *Self) void { + for (self.connections) |*conn_opt| { + if (conn_opt.*) |*conn| { + conn.close(); + } + } + self.allocator.free(self.connections); + self.allocator.free(self.in_use); + self.allocator.free(self.path); + } + + /// Acquires a connection from the pool. + /// + /// Returns an existing idle connection or creates a new one if the pool + /// isn't full. Returns error if all connections are in use and pool is full. + pub fn acquire(self: *Self) !*Database { + self.mutex.lock(); + defer self.mutex.unlock(); + + // Look for an existing idle connection + for (self.connections, 0..) |*conn_opt, i| { + if (conn_opt.* != null and !self.in_use[i]) { + self.in_use[i] = true; + return &conn_opt.*.?; + } + } + + // Look for an empty slot to create a new connection + for (self.connections, 0..) |*conn_opt, i| { + if (conn_opt.* == null) { + // Create null-terminated path + const path_z = self.allocator.dupeZ(u8, self.path) catch return error.OutOfMemory; + defer self.allocator.free(path_z); + + conn_opt.* = Database.open(path_z) catch |e| { + return e; + }; + self.in_use[i] = true; + return &conn_opt.*.?; + } + } + + return error.Busy; // Pool exhausted + } + + /// Releases a connection back to the pool. + pub fn release(self: *Self, conn: *Database) void { + self.mutex.lock(); + defer self.mutex.unlock(); + + for (self.connections, 0..) |*conn_opt, i| { + if (conn_opt.*) |*stored_conn| { + if (stored_conn == conn) { + self.in_use[i] = false; + return; + } + } + } + } + + /// Returns the number of connections currently in use. + pub fn inUseCount(self: *Self) usize { + self.mutex.lock(); + defer self.mutex.unlock(); + + var count: usize = 0; + for (self.in_use) |used| { + if (used) count += 1; + } + return count; + } + + /// Returns the total number of open connections (idle + in use). + pub fn openCount(self: *Self) usize { + self.mutex.lock(); + defer self.mutex.unlock(); + + var count: usize = 0; + for (self.connections) |conn_opt| { + if (conn_opt != null) count += 1; + } + return count; + } + + /// Returns the maximum pool size. + pub fn capacity(self: *Self) usize { + return self.max_size; + } +}; + // ============================================================================ // Tests // ============================================================================ @@ -3417,3 +3959,235 @@ test "timestamp binding named" { const created_at = query.columnText(0) orelse ""; try std.testing.expectEqualStrings("2020-06-15 12:00:00", created_at); } + +// Window function test helpers +const WindowSumState = struct { + total: i64 = 0, +}; + +fn windowSumStep(ctx: AggregateContext, args: []const FunctionValue) void { + const state = ctx.getAggregateContext(WindowSumState) orelse return; + if (args.len > 0 and !args[0].isNull()) { + state.total += args[0].asInt(); + } +} + +fn windowSumInverse(ctx: AggregateContext, args: []const FunctionValue) void { + const state = ctx.getAggregateContext(WindowSumState) orelse return; + if (args.len > 0 and !args[0].isNull()) { + state.total -= args[0].asInt(); + } +} + +fn windowSumValue(ctx: AggregateContext) void { + const state = ctx.getAggregateContext(WindowSumState) orelse { + ctx.setNull(); + return; + }; + ctx.setInt(state.total); +} + +fn windowSumFinal(ctx: AggregateContext) void { + windowSumValue(ctx); +} + +test "window function" { + var db = try openMemory(); + defer db.close(); + + // Register custom window function + try db.createWindowFunction("mysum", 1, windowSumStep, windowSumFinal, windowSumValue, windowSumInverse); + + try db.exec("CREATE TABLE nums (id INTEGER PRIMARY KEY, value INTEGER)"); + try db.exec("INSERT INTO nums (value) VALUES (1), (2), (3), (4), (5)"); + + // Test as regular aggregate (works for window functions too) + var stmt = try db.prepare("SELECT mysum(value) FROM nums"); + defer stmt.finalize(); + + _ = try stmt.step(); + try std.testing.expectEqual(@as(i64, 15), stmt.columnInt(0)); +} + +test "window function with OVER clause" { + var db = try openMemory(); + defer db.close(); + + try db.createWindowFunction("mysum", 1, windowSumStep, windowSumFinal, windowSumValue, windowSumInverse); + + try db.exec("CREATE TABLE nums (id INTEGER PRIMARY KEY, value INTEGER)"); + try db.exec("INSERT INTO nums (value) VALUES (1), (2), (3), (4), (5)"); + + // Running sum using OVER clause + var stmt = try db.prepare( + \\SELECT value, mysum(value) OVER (ORDER BY id ROWS UNBOUNDED PRECEDING) as running_sum + \\FROM nums ORDER BY id + ); + defer stmt.finalize(); + + // Expected: 1->1, 2->3, 3->6, 4->10, 5->15 + const expected_sums = [_]i64{ 1, 3, 6, 10, 15 }; + var i: usize = 0; + while (try stmt.step()) { + const running_sum = stmt.columnInt(1); + try std.testing.expectEqual(expected_sums[i], running_sum); + i += 1; + } + try std.testing.expectEqual(@as(usize, 5), i); +} + +test "URI connection string" { + // Test in-memory URI + var db = try openUri("file::memory:"); + defer db.close(); + + try db.exec("CREATE TABLE test (x INTEGER)"); + try db.exec("INSERT INTO test VALUES (42)"); + + var stmt = try db.prepare("SELECT x FROM test"); + defer stmt.finalize(); + + _ = try stmt.step(); + try std.testing.expectEqual(@as(i64, 42), stmt.columnInt(0)); +} + +test "URI connection with mode parameter" { + // Create a temp file first + var db = try openUri("file::memory:?cache=private"); + defer db.close(); + + try db.exec("CREATE TABLE test (x TEXT)"); + try db.exec("INSERT INTO test VALUES ('hello')"); + + var stmt = try db.prepare("SELECT x FROM test"); + defer stmt.finalize(); + + _ = try stmt.step(); + try std.testing.expectEqualStrings("hello", stmt.columnText(0).?); +} + +test "pragma cache size" { + const allocator = std.testing.allocator; + var db = try openMemory(); + defer db.close(); + + // Set cache size to 2MB (negative means KB) + try db.setCacheSize(allocator, -2000); + + // Verify by creating table and inserting data + try db.exec("CREATE TABLE test (x INTEGER)"); + try db.exec("INSERT INTO test VALUES (1)"); +} + +test "pragma query only" { + var db = try openMemory(); + defer db.close(); + + try db.exec("CREATE TABLE test (x INTEGER)"); + try db.exec("INSERT INTO test VALUES (1)"); + + // Enable query only + try db.setQueryOnly(true); + + // Reads should work + var stmt = try db.prepare("SELECT x FROM test"); + defer stmt.finalize(); + _ = try stmt.step(); + try std.testing.expectEqual(@as(i64, 1), stmt.columnInt(0)); + + // Writes should fail + const result = db.exec("INSERT INTO test VALUES (2)"); + try std.testing.expectError(Error.ReadOnly, result); + + // Disable query only + try db.setQueryOnly(false); + try db.exec("INSERT INTO test VALUES (2)"); +} + +test "integrity check" { + 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), (2), (3)"); + + const result = try db.integrityCheck(allocator); + defer allocator.free(result); + + try std.testing.expectEqualStrings("ok", result); +} + +test "vacuum" { + var db = try openMemory(); + defer db.close(); + + try db.exec("CREATE TABLE test (x INTEGER)"); + try db.exec("INSERT INTO test VALUES (1), (2), (3)"); + try db.exec("DELETE FROM test"); + + // VACUUM should not error + try db.vacuum(); +} + +test "optimize" { + var db = try openMemory(); + defer db.close(); + + try db.exec("CREATE TABLE test (x INTEGER)"); + try db.exec("INSERT INTO test VALUES (1), (2), (3)"); + + // Optimize should not error + try db.optimize(); +} + +test "connection pool basic" { + const allocator = std.testing.allocator; + + // Use shared memory database for testing + var pool = try ConnectionPool.init(allocator, "file::memory:?cache=shared", 3); + defer pool.deinit(); + + try std.testing.expectEqual(@as(usize, 3), pool.capacity()); + try std.testing.expectEqual(@as(usize, 0), pool.openCount()); + + // Acquire first connection + const conn1 = try pool.acquire(); + try std.testing.expectEqual(@as(usize, 1), pool.openCount()); + try std.testing.expectEqual(@as(usize, 1), pool.inUseCount()); + + // Create table + try conn1.exec("CREATE TABLE test (x INTEGER)"); + try conn1.exec("INSERT INTO test VALUES (1)"); + + // Acquire second connection + const conn2 = try pool.acquire(); + try std.testing.expectEqual(@as(usize, 2), pool.openCount()); + try std.testing.expectEqual(@as(usize, 2), pool.inUseCount()); + + // Release first connection + pool.release(conn1); + try std.testing.expectEqual(@as(usize, 2), pool.openCount()); + try std.testing.expectEqual(@as(usize, 1), pool.inUseCount()); + + // Release second connection + pool.release(conn2); + try std.testing.expectEqual(@as(usize, 0), pool.inUseCount()); +} + +test "connection pool reuse" { + const allocator = std.testing.allocator; + + var pool = try ConnectionPool.init(allocator, "file::memory:?cache=shared", 2); + defer pool.deinit(); + + // Acquire and release + const conn1 = try pool.acquire(); + pool.release(conn1); + + // Acquire again - should reuse the same connection + const conn2 = try pool.acquire(); + try std.testing.expectEqual(@as(usize, 1), pool.openCount()); + + pool.release(conn2); +}