Fase 4: Window functions, URI, pragmas y connection pool

- 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 <noreply@anthropic.com>
This commit is contained in:
reugenio 2025-12-08 19:33:46 +01:00
parent 733533ec83
commit 7229c27c80
4 changed files with 994 additions and 19 deletions

View file

@ -1,6 +1,6 @@
# zsqlite - API Reference # zsqlite - API Reference
> **Version**: 0.5 > **Version**: 0.6
> **Ultima actualizacion**: 2025-12-08 > **Ultima actualizacion**: 2025-12-08
## Quick Reference ## Quick Reference
@ -11,8 +11,15 @@ const sqlite = @import("zsqlite");
// Abrir base de datos // Abrir base de datos
var db = try sqlite.openMemory(); // In-memory var db = try sqlite.openMemory(); // In-memory
var db = try sqlite.open("file.db"); // Archivo var db = try sqlite.open("file.db"); // Archivo
var db = try sqlite.openUri("file:test.db?mode=ro"); // URI
defer db.close(); 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 // SQL directo
try db.exec("CREATE TABLE ..."); try db.exec("CREATE TABLE ...");
@ -46,6 +53,7 @@ var restored = try sqlite.loadFromFile("backup.db");
// User-defined functions // User-defined functions
try db.createScalarFunction("double", 1, myDoubleFunc); try db.createScalarFunction("double", 1, myDoubleFunc);
try db.createAggregateFunction("sum_squares", 1, stepFn, finalFn); try db.createAggregateFunction("sum_squares", 1, stepFn, finalFn);
try db.createWindowFunction("running_sum", 1, stepFn, finalFn, valueFn, inverseFn);
// Custom collations // Custom collations
try db.createCollation("NOCASE2", myCaseInsensitiveCompare); try db.createCollation("NOCASE2", myCaseInsensitiveCompare);
@ -70,6 +78,16 @@ try stmt.bindCurrentTime(1);
// Limits // Limits
const old = db.setLimit(.sql_length, 10000); const old = db.setLimit(.sql_length, 10000);
const current = db.getLimit(.sql_length); 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) | | `open(path)` | Abre conexion (read-write, create) |
| `openWithFlags(path, flags)` | Abre con flags especificos | | `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 | | `close()` | Cierra la conexion |
### Ejecucion SQL ### Ejecucion SQL
@ -200,6 +220,29 @@ try db.commit();
| `setJournalMode(alloc, mode)` | "WAL", "DELETE", etc | | `setJournalMode(alloc, mode)` | "WAL", "DELETE", etc |
| `setSynchronous(alloc, mode)` | "OFF", "NORMAL", "FULL" | | `setSynchronous(alloc, mode)` | "OFF", "NORMAL", "FULL" |
| `enableWalMode(alloc)` | WAL + NORMAL sync | | `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 ### 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&param2=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* *2025-12-08*

View file

@ -1,6 +1,6 @@
# zsqlite - Arquitectura Tecnica # zsqlite - Arquitectura Tecnica
> **Version**: 0.3 > **Version**: 0.6
> **Ultima actualizacion**: 2025-12-08 > **Ultima actualizacion**: 2025-12-08
## Vision General ## Vision General
@ -13,8 +13,8 @@ zsqlite es un wrapper de SQLite para Zig que compila SQLite amalgamation directa
├─────────────────────────────────────────────────────────────┤ ├─────────────────────────────────────────────────────────────┤
│ zsqlite API │ │ zsqlite API │
│ ┌──────────┐ ┌───────────┐ ┌──────────┐ ┌───────────┐ │ │ ┌──────────┐ ┌───────────┐ ┌──────────┐ ┌───────────┐ │
│ │ Database │ │ Statement │ │ Error │ │ Column │ │ │ │ Database │ │ Statement │ │ Backup │ │ConnPool │ │
│ │ │ │ │ │ Mapping │ │ Type │ │ │ │ │ │ │ │ Blob │ │ Functions │ │
│ └────┬─────┘ └─────┬─────┘ └────┬─────┘ └─────┬─────┘ │ │ └────┬─────┘ └─────┬─────┘ └────┬─────┘ └─────┬─────┘ │
├───────┼──────────────┼─────────────┼──────────────┼────────┤ ├───────┼──────────────┼─────────────┼──────────────┼────────┤
│ │ │ │ │ │ │ │ │ │ │ │

View file

@ -27,9 +27,9 @@ Este documento analiza todas sus funcionalidades para asegurar paridad en zsqlit
| Open basico | ✅ | ✅ | `sqlite.open()` | | Open basico | ✅ | ✅ | `sqlite.open()` |
| Open con flags | ✅ | ✅ | `Database.openWithFlags()` | | Open con flags | ✅ | ✅ | `Database.openWithFlags()` |
| Close | ✅ | ✅ | `db.close()` | | Close | ✅ | ✅ | `db.close()` |
| URI connection string | ✅ | ⏳ | Parsear `file:path?mode=ro&cache=shared` | | URI connection string | ✅ | ✅ | `db.openUri()` soporta `file:path?mode=ro&cache=shared` |
| DSN parameters | ✅ | ⏳ | Configuracion via string | | DSN parameters | ✅ | ✅ | Via URI connection string |
| Connection pooling | ✅ (via database/sql) | ⏳ | Pool propio | | 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 | | Pragma | go-sqlite3 | zsqlite | Prioridad |
|--------|------------|---------|-----------| |--------|------------|---------|-----------|
| auto_vacuum | ✅ | ⏳ | Media | | auto_vacuum | ✅ | ✅ | `db.setAutoVacuum()` |
| busy_timeout | ✅ | ✅ | `db.setBusyTimeout()` | | busy_timeout | ✅ | ✅ | `db.setBusyTimeout()` |
| cache_size | ✅ | ⏳ | Media | | cache_size | ✅ | ✅ | `db.setCacheSize()` |
| case_sensitive_like | ✅ | ⏳ | Baja | | case_sensitive_like | ✅ | ✅ | `db.setCaseSensitiveLike()` |
| defer_foreign_keys | ✅ | ⏳ | Media | | defer_foreign_keys | ✅ | ✅ | `db.setDeferForeignKeys()` |
| foreign_keys | ✅ | ✅ | `db.setForeignKeys()` | | foreign_keys | ✅ | ✅ | `db.setForeignKeys()` |
| journal_mode | ✅ | ✅ | `db.setJournalMode()` | | journal_mode | ✅ | ✅ | `db.setJournalMode()` |
| locking_mode | ✅ | ⏳ | Media | | locking_mode | ✅ | ✅ | `db.setLockingMode()` |
| query_only | ✅ | ⏳ | Baja | | query_only | ✅ | ✅ | `db.setQueryOnly()` |
| recursive_triggers | ✅ | ⏳ | Baja | | recursive_triggers | ✅ | ✅ | `db.setRecursiveTriggers()` |
| secure_delete | ✅ | ⏳ | Baja | | secure_delete | ✅ | ✅ | `db.setSecureDelete()` |
| synchronous | ✅ | ✅ | `db.setSynchronous()` | | synchronous | ✅ | ✅ | `db.setSynchronous()` |
--- ---
@ -170,7 +170,7 @@ Este documento analiza todas sus funcionalidades para asegurar paridad en zsqlit
| RegisterFunc (scalar) | ✅ | ✅ | `db.createScalarFunction()` | | RegisterFunc (scalar) | ✅ | ✅ | `db.createScalarFunction()` |
| RegisterAggregator | ✅ | ✅ | `db.createAggregateFunction()` | | RegisterAggregator | ✅ | ✅ | `db.createAggregateFunction()` |
| RegisterCollation | ✅ | ✅ | `db.createCollation()` | | 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()` 1. ✅ Blob I/O streaming - `Blob` struct con `read()`, `write()`, `reopen()`
2. ✅ Hooks (commit, rollback, update) - `db.setCommitHook()`, etc. 2. ✅ Hooks (commit, rollback, update) - `db.setCommitHook()`, etc.
3. ✅ Aggregator functions - `db.createAggregateFunction()` 3. ✅ Aggregator functions - `db.createAggregateFunction()`
4. Mas pragmas 4. Mas pragmas
### Fase 3B - Prioridad Baja ✅ COMPLETADA ### Fase 3B - Prioridad Baja ✅ COMPLETADA
1. ✅ Authorizer - `db.setAuthorizer()` 1. ✅ Authorizer - `db.setAuthorizer()`
@ -238,7 +238,14 @@ Este documento analiza todas sus funcionalidades para asegurar paridad en zsqlit
6. ✅ Timestamp binding - `stmt.bindTimestamp()` 6. ✅ Timestamp binding - `stmt.bindTimestamp()`
7. ✅ Column metadata - `stmt.columnDatabaseName()`, etc. 7. ✅ Column metadata - `stmt.columnDatabaseName()`, etc.
8. ✅ Expanded SQL - `stmt.expandedSql()` 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()`
--- ---

View file

@ -169,6 +169,44 @@ pub const Database = struct {
return .{ .handle = handle }; 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. /// Closes the database connection.
pub fn close(self: *Self) void { pub fn close(self: *Self) void {
if (self.handle) |h| { if (self.handle) |h| {
@ -345,6 +383,163 @@ pub const Database = struct {
try self.setSynchronous(allocator, "NORMAL"); 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. /// Interrupts a long-running query.
/// ///
/// Causes any pending database operation to abort and return SQLITE_INTERRUPT. /// 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 // 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 // Custom Collations
// ============================================================================ // ============================================================================
@ -2398,6 +2783,15 @@ pub fn openMemory() Error!Database {
return Database.open(":memory:"); 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. /// Returns the SQLite version string.
pub fn version() []const u8 { pub fn version() []const u8 {
return std.mem.span(c.sqlite3_libversion()); return std.mem.span(c.sqlite3_libversion());
@ -2408,6 +2802,154 @@ pub fn versionNumber() i32 {
return c.sqlite3_libversion_number(); 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 // Tests
// ============================================================================ // ============================================================================
@ -3417,3 +3959,235 @@ test "timestamp binding named" {
const created_at = query.columnText(0) orelse ""; const created_at = query.columnText(0) orelse "";
try std.testing.expectEqualStrings("2020-06-15 12:00:00", created_at); 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);
}