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
|
||||
|
||||
> **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*
|
||||
|
|
|
|||
|
|
@ -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 │ │
|
||||
│ └────┬─────┘ └─────┬─────┘ └────┬─────┘ └─────┬─────┘ │
|
||||
├───────┼──────────────┼─────────────┼──────────────┼────────┤
|
||||
│ │ │ │ │ │
|
||||
|
|
|
|||
|
|
@ -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()`
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
774
src/root.zig
774
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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue