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:
parent
733533ec83
commit
7229c27c80
4 changed files with 994 additions and 19 deletions
198
docs/API.md
198
docs/API.md
|
|
@ -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¶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*
|
*2025-12-08*
|
||||||
|
|
|
||||||
|
|
@ -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 │ │
|
||||||
│ └────┬─────┘ └─────┬─────┘ └────┬─────┘ └─────┬─────┘ │
|
│ └────┬─────┘ └─────┬─────┘ └────┬─────┘ └─────┬─────┘ │
|
||||||
├───────┼──────────────┼─────────────┼──────────────┼────────┤
|
├───────┼──────────────┼─────────────┼──────────────┼────────┤
|
||||||
│ │ │ │ │ │
|
│ │ │ │ │ │
|
||||||
|
|
|
||||||
|
|
@ -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()`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
774
src/root.zig
774
src/root.zig
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue