feat(v0.5): Fase 3B - Callbacks avanzados, limits y timestamps
Implementa todas las funcionalidades restantes de la paridad con go-sqlite3: Callbacks y Hooks: - Authorizer callback para control de operaciones SQL - Pre-update hook con acceso a valores antes/despues del cambio - Progress handler para interrumpir queries largos - Busy handler personalizado (custom callback) APIs adicionales: - Limits API (getLimit/setLimit) para control de limites SQLite - Column metadata extendida (columnDatabaseName, columnTableName, columnOriginName) - Expanded SQL (stmt.expandedSql) - Timestamp binding (bindTimestamp, bindCurrentTime) con formato ISO8601 Build: - Habilitado SQLITE_ENABLE_PREUPDATE_HOOK en build.zig - Definido @cDefine en @cImport para exponer APIs opcionales Tests: - Tests para authorizer, progress handler, limits, expanded SQL - Tests para column metadata y pre-update hook - Tests para timestamp binding Documentacion actualizada con todos los nuevos APIs y ejemplos. 🤖 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
7742f44667
commit
733533ec83
4 changed files with 1054 additions and 20 deletions
|
|
@ -17,6 +17,8 @@ pub fn build(b: *std.Build) void {
|
|||
"-DSQLITE_ENABLE_JSON1", // JSON functions
|
||||
"-DSQLITE_ENABLE_RTREE", // R-Tree for geospatial
|
||||
"-DSQLITE_OMIT_LOAD_EXTENSION", // No dynamic extensions
|
||||
"-DSQLITE_ENABLE_COLUMN_METADATA", // Column metadata functions
|
||||
"-DSQLITE_ENABLE_PREUPDATE_HOOK", // Pre-update hook API
|
||||
};
|
||||
|
||||
// zsqlite module - includes SQLite C compilation
|
||||
|
|
|
|||
245
docs/API.md
245
docs/API.md
|
|
@ -1,6 +1,6 @@
|
|||
# zsqlite - API Reference
|
||||
|
||||
> **Version**: 0.4
|
||||
> **Version**: 0.5
|
||||
> **Ultima actualizacion**: 2025-12-08
|
||||
|
||||
## Quick Reference
|
||||
|
|
@ -59,6 +59,17 @@ try blob.read(&buffer, 0);
|
|||
// Hooks
|
||||
try db.setCommitHook(myCommitHook);
|
||||
try db.setUpdateHook(myUpdateHook);
|
||||
try db.setPreUpdateHook(myPreUpdateHook);
|
||||
try db.setAuthorizer(myAuthorizer);
|
||||
try db.setProgressHandler(1000, myProgress);
|
||||
|
||||
// Timestamp binding
|
||||
try stmt.bindTimestamp(1, unix_timestamp);
|
||||
try stmt.bindCurrentTime(1);
|
||||
|
||||
// Limits
|
||||
const old = db.setLimit(.sql_length, 10000);
|
||||
const current = db.getLimit(.sql_length);
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -265,6 +276,21 @@ try db.createCollation("REVERSE", reverseOrder);
|
|||
| `interrupt()` | Interrumpe operacion |
|
||||
| `isReadOnly(db_name)` | Si DB es readonly |
|
||||
| `filename(db_name)` | Ruta del archivo |
|
||||
| `getLimit(limit_type)` | Obtener limite actual |
|
||||
| `setLimit(limit_type, val)` | Establecer limite |
|
||||
|
||||
### Hooks y Callbacks
|
||||
|
||||
| Funcion | Descripcion |
|
||||
|---------|-------------|
|
||||
| `setCommitHook(fn)` | Hook al commit |
|
||||
| `setRollbackHook(fn)` | Hook al rollback |
|
||||
| `setUpdateHook(fn)` | Hook a INSERT/UPDATE/DELETE |
|
||||
| `setPreUpdateHook(fn)` | Hook ANTES de cambios |
|
||||
| `setAuthorizer(fn)` | Autorizar operaciones SQL |
|
||||
| `setProgressHandler(n, fn)` | Callback cada N operaciones |
|
||||
| `setBusyHandler(fn)` | Handler personalizado de busy |
|
||||
| `clearHooks()` | Eliminar todos los hooks |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -311,6 +337,10 @@ try db.createCollation("REVERSE", reverseOrder);
|
|||
| `bindTextNamed(name, val)` | |
|
||||
| `bindBlobNamed(name, val)` | |
|
||||
| `bindBoolNamed(name, val)` | |
|
||||
| `bindTimestamp(idx, ts)` | Unix timestamp como ISO8601 |
|
||||
| `bindTimestampNamed(name, ts)` | |
|
||||
| `bindCurrentTime(idx)` | Tiempo actual como ISO8601 |
|
||||
| `bindCurrentTimeNamed(name)` | |
|
||||
|
||||
### Column Access (0-indexed)
|
||||
|
||||
|
|
@ -327,6 +357,15 @@ try db.createCollation("REVERSE", reverseOrder);
|
|||
| `columnIsNull(idx)` | bool |
|
||||
| `columnBytes(idx)` | Tamano en bytes |
|
||||
| `columnDeclType(idx)` | Tipo declarado |
|
||||
| `columnDatabaseName(idx)` | Nombre de la base de datos |
|
||||
| `columnTableName(idx)` | Nombre de la tabla |
|
||||
| `columnOriginName(idx)` | Nombre original de la columna |
|
||||
|
||||
### Statement Metadata Extended
|
||||
|
||||
| Funcion | Descripcion |
|
||||
|---------|-------------|
|
||||
| `expandedSql(allocator)` | SQL con parametros expandidos |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -694,5 +733,207 @@ pub fn createAggregateFunction(
|
|||
|
||||
---
|
||||
|
||||
**© zsqlite v0.4 - API Reference**
|
||||
## Authorizer
|
||||
|
||||
Controla que operaciones SQL estan permitidas.
|
||||
|
||||
### Types
|
||||
|
||||
```zig
|
||||
pub const AuthAction = enum(i32) {
|
||||
create_index, create_table, create_temp_index, create_temp_table,
|
||||
create_temp_trigger, create_temp_view, create_trigger, create_view,
|
||||
delete, drop_index, drop_table, drop_temp_index, drop_temp_table,
|
||||
drop_temp_trigger, drop_temp_view, drop_trigger, drop_view,
|
||||
insert, pragma, read, select, transaction, update,
|
||||
attach, detach, alter_table, reindex, analyze,
|
||||
create_vtable, drop_vtable, function, savepoint, recursive,
|
||||
};
|
||||
|
||||
pub const AuthResult = enum(i32) {
|
||||
ok, // Permitir
|
||||
deny, // Denegar con error
|
||||
ignore, // Tratar como NULL
|
||||
};
|
||||
|
||||
pub const ZigAuthorizerFn = *const fn (
|
||||
action: AuthAction,
|
||||
arg1: ?[]const u8, // tabla/indice
|
||||
arg2: ?[]const u8, // columna/trigger
|
||||
arg3: ?[]const u8, // nombre de database
|
||||
arg4: ?[]const u8, // trigger/view
|
||||
) AuthResult;
|
||||
```
|
||||
|
||||
### Ejemplo
|
||||
|
||||
```zig
|
||||
fn myAuthorizer(action: AuthAction, arg1: ?[]const u8, _, _, _) AuthResult {
|
||||
if (action == .drop_table) {
|
||||
if (arg1) |table| {
|
||||
if (std.mem.eql(u8, table, "important")) return .deny;
|
||||
}
|
||||
}
|
||||
return .ok;
|
||||
}
|
||||
|
||||
try db.setAuthorizer(myAuthorizer);
|
||||
// Ahora DROP TABLE important fallara
|
||||
try db.setAuthorizer(null); // Remover
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pre-Update Hook
|
||||
|
||||
Hook que se ejecuta ANTES de cambios, permitiendo acceso a valores antiguos y nuevos.
|
||||
|
||||
### Types
|
||||
|
||||
```zig
|
||||
pub const PreUpdateContext = struct {
|
||||
pub fn columnCount(self: Self) i32
|
||||
pub fn depth(self: Self) i32 // 0=directo, 1=trigger, etc
|
||||
pub fn oldValue(self: Self, col: u32) ?FunctionValue // UPDATE/DELETE
|
||||
pub fn newValue(self: Self, col: u32) ?FunctionValue // UPDATE/INSERT
|
||||
};
|
||||
|
||||
pub const ZigPreUpdateHookFn = *const fn (
|
||||
ctx: PreUpdateContext,
|
||||
operation: UpdateOperation,
|
||||
db_name: []const u8,
|
||||
table_name: []const u8,
|
||||
old_rowid: i64,
|
||||
new_rowid: i64,
|
||||
) void;
|
||||
```
|
||||
|
||||
### Ejemplo
|
||||
|
||||
```zig
|
||||
fn auditHook(ctx: PreUpdateContext, op: UpdateOperation, _, table: []const u8, _, _) void {
|
||||
if (op == .update) {
|
||||
// Acceder a valor antes del cambio
|
||||
if (ctx.oldValue(0)) |old| {
|
||||
const old_val = old.asInt();
|
||||
// Acceder a nuevo valor
|
||||
if (ctx.newValue(0)) |new| {
|
||||
const new_val = new.asInt();
|
||||
std.debug.print("{s}: {d} -> {d}\n", .{table, old_val, new_val});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try db.setPreUpdateHook(auditHook);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Progress Handler
|
||||
|
||||
Callback periodico para queries de larga duracion.
|
||||
|
||||
```zig
|
||||
pub const ZigProgressFn = *const fn () bool; // true=continuar, false=interrumpir
|
||||
```
|
||||
|
||||
### Ejemplo
|
||||
|
||||
```zig
|
||||
var should_cancel = false;
|
||||
|
||||
fn checkCancel() bool {
|
||||
return !should_cancel; // false interrumpe el query
|
||||
}
|
||||
|
||||
try db.setProgressHandler(1000, checkCancel); // Cada 1000 operaciones VM
|
||||
// Para cancelar un query largo:
|
||||
// should_cancel = true;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Busy Handler
|
||||
|
||||
Handler personalizado para cuando la base de datos esta bloqueada.
|
||||
|
||||
```zig
|
||||
pub const ZigBusyHandlerFn = *const fn (count: i32) bool; // true=reintentar
|
||||
```
|
||||
|
||||
### Ejemplo
|
||||
|
||||
```zig
|
||||
fn myBusyHandler(count: i32) bool {
|
||||
if (count > 10) return false; // Fallar despues de 10 reintentos
|
||||
std.time.sleep(100_000_000); // Esperar 100ms
|
||||
return true; // Reintentar
|
||||
}
|
||||
|
||||
try db.setBusyHandler(myBusyHandler);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Limits
|
||||
|
||||
Control de limites de SQLite.
|
||||
|
||||
### Limit Types
|
||||
|
||||
```zig
|
||||
pub const Limit = enum(i32) {
|
||||
length, // Tamano maximo de string/blob
|
||||
sql_length, // Longitud maxima de SQL
|
||||
column, // Columnas por tabla/query
|
||||
expr_depth, // Profundidad de expresiones
|
||||
compound_select, // Terminos en SELECT compuesto
|
||||
vdbe_op, // Operaciones de VM
|
||||
function_arg, // Argumentos de funcion
|
||||
attached, // Databases attached
|
||||
like_pattern_length, // Patron LIKE
|
||||
variable_number, // Variables SQL
|
||||
trigger_depth, // Profundidad de triggers
|
||||
worker_threads, // Threads de trabajo
|
||||
};
|
||||
```
|
||||
|
||||
### Ejemplo
|
||||
|
||||
```zig
|
||||
// Obtener limite actual
|
||||
const sql_limit = db.getLimit(.sql_length);
|
||||
|
||||
// Establecer nuevo limite (retorna el anterior)
|
||||
const old_limit = db.setLimit(.sql_length, 10000);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Timestamp Binding
|
||||
|
||||
Bind de timestamps Unix como texto ISO8601 (YYYY-MM-DD HH:MM:SS).
|
||||
|
||||
### Funciones
|
||||
|
||||
```zig
|
||||
pub fn bindTimestamp(self: *Statement, index: u32, ts: i64) Error!void
|
||||
pub fn bindTimestampNamed(self: *Statement, name: [:0]const u8, ts: i64) Error!void
|
||||
pub fn bindCurrentTime(self: *Statement, index: u32) Error!void
|
||||
pub fn bindCurrentTimeNamed(self: *Statement, name: [:0]const u8) Error!void
|
||||
```
|
||||
|
||||
### Ejemplo
|
||||
|
||||
```zig
|
||||
var stmt = try db.prepare("INSERT INTO events (created_at) VALUES (?)");
|
||||
try stmt.bindTimestamp(1, 1705314645); // 2024-01-15 10:30:45 UTC
|
||||
// O usar tiempo actual
|
||||
try stmt.bindCurrentTime(1);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**© zsqlite v0.5 - API Reference**
|
||||
*2025-12-08*
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ Este documento analiza todas sus funcionalidades para asegurar paridad en zsqlit
|
|||
| Bind named ($name) | ✅ | ✅ | `stmt.bindIntNamed("$name", val)` |
|
||||
| Readonly check | ✅ | ✅ | `stmt.isReadOnly()` |
|
||||
| SQL text | ✅ | ✅ | `stmt.sql()` |
|
||||
| Expanded SQL | ✅ | ⏳ | `sqlite3_expanded_sql()` |
|
||||
| Expanded SQL | ✅ | ✅ | `stmt.expandedSql()` |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -79,7 +79,7 @@ Este documento analiza todas sus funcionalidades para asegurar paridad en zsqlit
|
|||
| string/text | ✅ | ✅ | `stmt.bindText()` |
|
||||
| []byte/blob | ✅ | ✅ | `stmt.bindBlob()` |
|
||||
| bool | ✅ | ✅ | `stmt.bindBool()` |
|
||||
| time.Time | ✅ | ⏳ | Formatear como string ISO8601 |
|
||||
| time.Time | ✅ | ✅ | `stmt.bindTimestamp()` ISO8601 |
|
||||
| Zeroblob | ✅ | ✅ | `stmt.bindZeroblob()` |
|
||||
|
||||
---
|
||||
|
|
@ -98,9 +98,9 @@ Este documento analiza todas sus funcionalidades para asegurar paridad en zsqlit
|
|||
| NULL check | ✅ | ✅ | `stmt.columnIsNull()` |
|
||||
| Column bytes | ✅ | ✅ | `stmt.columnBytes()` |
|
||||
| Declared type | ✅ | ✅ | `stmt.columnDeclType()` |
|
||||
| Database name | ✅ | ⏳ | `sqlite3_column_database_name()` |
|
||||
| Table name | ✅ | ⏳ | `sqlite3_column_table_name()` |
|
||||
| Origin name | ✅ | ⏳ | `sqlite3_column_origin_name()` |
|
||||
| Database name | ✅ | ✅ | `stmt.columnDatabaseName()` |
|
||||
| Table name | ✅ | ✅ | `stmt.columnTableName()` |
|
||||
| Origin name | ✅ | ✅ | `stmt.columnOriginName()` |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -141,8 +141,8 @@ Este documento analiza todas sus funcionalidades para asegurar paridad en zsqlit
|
|||
|
||||
| Funcionalidad | go-sqlite3 | zsqlite | Prioridad |
|
||||
|---------------|------------|---------|-----------|
|
||||
| GetLimit | ✅ | ⏳ | Baja |
|
||||
| SetLimit | ✅ | ⏳ | Baja |
|
||||
| GetLimit | ✅ | ✅ | `db.getLimit()` |
|
||||
| SetLimit | ✅ | ✅ | `db.setLimit()` |
|
||||
| SetFileControlInt | ✅ | ⏳ | Baja |
|
||||
| Interrupt | ✅ | ✅ | `db.interrupt()` |
|
||||
|
||||
|
|
@ -155,10 +155,10 @@ Este documento analiza todas sus funcionalidades para asegurar paridad en zsqlit
|
|||
| Commit hook | ✅ | ✅ | `db.setCommitHook()` |
|
||||
| Rollback hook | ✅ | ✅ | `db.setRollbackHook()` |
|
||||
| Update hook | ✅ | ✅ | `db.setUpdateHook()` |
|
||||
| Pre-update hook | ✅ | ⏳ | Baja |
|
||||
| Authorizer | ✅ | ⏳ | Baja |
|
||||
| Progress handler | ✅ | ⏳ | Baja |
|
||||
| Busy handler | ✅ | ⏳ | Media |
|
||||
| Pre-update hook | ✅ | ✅ | `db.setPreUpdateHook()` |
|
||||
| Authorizer | ✅ | ✅ | `db.setAuthorizer()` |
|
||||
| Progress handler | ✅ | ✅ | `db.setProgressHandler()` |
|
||||
| Busy handler | ✅ | ✅ | `db.setBusyHandler()` |
|
||||
| Busy timeout | ✅ | ✅ | `db.setBusyTimeout()` |
|
||||
|
||||
---
|
||||
|
|
@ -229,13 +229,16 @@ Este documento analiza todas sus funcionalidades para asegurar paridad en zsqlit
|
|||
3. ✅ Aggregator functions - `db.createAggregateFunction()`
|
||||
4. ⏳ Mas pragmas
|
||||
|
||||
### Fase 3B - Prioridad Baja (Siguiente)
|
||||
1. ⏳ Authorizer
|
||||
2. ⏳ Progress handler
|
||||
3. ⏳ Pre-update hook
|
||||
4. ⏳ Window functions
|
||||
5. ⏳ Limits API
|
||||
6. ⏳ Busy handler (custom callback)
|
||||
### Fase 3B - Prioridad Baja ✅ COMPLETADA
|
||||
1. ✅ Authorizer - `db.setAuthorizer()`
|
||||
2. ✅ Progress handler - `db.setProgressHandler()`
|
||||
3. ✅ Pre-update hook - `db.setPreUpdateHook()`
|
||||
4. ✅ Limits API - `db.getLimit()`, `db.setLimit()`
|
||||
5. ✅ Busy handler (custom callback) - `db.setBusyHandler()`
|
||||
6. ✅ Timestamp binding - `stmt.bindTimestamp()`
|
||||
7. ✅ Column metadata - `stmt.columnDatabaseName()`, etc.
|
||||
8. ✅ Expanded SQL - `stmt.expandedSql()`
|
||||
9. ⏳ Window functions (baja prioridad)
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
788
src/root.zig
788
src/root.zig
|
|
@ -24,6 +24,9 @@
|
|||
|
||||
const std = @import("std");
|
||||
const c = @cImport({
|
||||
// Define compile flags needed for all features
|
||||
@cDefine("SQLITE_ENABLE_COLUMN_METADATA", "1");
|
||||
@cDefine("SQLITE_ENABLE_PREUPDATE_HOOK", "1");
|
||||
@cInclude("sqlite3.h");
|
||||
});
|
||||
|
||||
|
|
@ -710,6 +713,44 @@ pub const Database = struct {
|
|||
}
|
||||
}
|
||||
|
||||
/// Sets a pre-update hook callback.
|
||||
///
|
||||
/// The pre-update hook is invoked BEFORE each INSERT, UPDATE, and DELETE operation.
|
||||
/// Unlike the regular update hook, this allows access to the old and new values
|
||||
/// through the PreUpdateContext parameter.
|
||||
///
|
||||
/// Pass null to remove an existing hook.
|
||||
///
|
||||
/// Note: Requires SQLITE_ENABLE_PREUPDATE_HOOK compile flag (enabled by default).
|
||||
///
|
||||
/// Example:
|
||||
/// ```zig
|
||||
/// fn myPreUpdateHook(ctx: PreUpdateContext, op: UpdateOperation, ...) void {
|
||||
/// if (op == .update) {
|
||||
/// const old_val = ctx.oldValue(0);
|
||||
/// const new_val = ctx.newValue(0);
|
||||
/// // Log the change...
|
||||
/// }
|
||||
/// }
|
||||
/// try db.setPreUpdateHook(myPreUpdateHook);
|
||||
/// ```
|
||||
pub fn setPreUpdateHook(self: *Self, func: ?ZigPreUpdateHookFn) !void {
|
||||
if (func) |f| {
|
||||
const wrapper = try PreUpdateHookWrapper.create(f);
|
||||
const old = c.sqlite3_preupdate_hook(self.handle, preUpdateHookCallback, wrapper);
|
||||
if (old != null) {
|
||||
const old_wrapper: *PreUpdateHookWrapper = @ptrCast(@alignCast(old));
|
||||
old_wrapper.destroy();
|
||||
}
|
||||
} else {
|
||||
const old = c.sqlite3_preupdate_hook(self.handle, null, null);
|
||||
if (old != null) {
|
||||
const old_wrapper: *PreUpdateHookWrapper = @ptrCast(@alignCast(old));
|
||||
old_wrapper.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes all hooks (commit, rollback, update).
|
||||
pub fn clearHooks(self: *Self) void {
|
||||
const commit_old = c.sqlite3_commit_hook(self.handle, null, null);
|
||||
|
|
@ -730,6 +771,113 @@ pub const Database = struct {
|
|||
wrapper.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Authorizer
|
||||
// ========================================================================
|
||||
|
||||
/// Sets an authorizer callback.
|
||||
///
|
||||
/// The authorizer is invoked for each SQL statement to determine
|
||||
/// whether the operation should be allowed.
|
||||
///
|
||||
/// Pass null to remove an existing authorizer.
|
||||
///
|
||||
/// Example:
|
||||
/// ```zig
|
||||
/// fn myAuthorizer(action: AuthAction, arg1: ?[]const u8, ...) AuthResult {
|
||||
/// if (action == .drop_table) return .deny;
|
||||
/// return .ok;
|
||||
/// }
|
||||
/// try db.setAuthorizer(myAuthorizer);
|
||||
/// ```
|
||||
pub fn setAuthorizer(self: *Self, func: ?ZigAuthorizerFn) !void {
|
||||
if (func) |f| {
|
||||
const wrapper = try AuthorizerWrapper.create(f);
|
||||
const result = c.sqlite3_set_authorizer(self.handle, authorizerCallback, wrapper);
|
||||
if (result != c.SQLITE_OK) {
|
||||
wrapper.destroy();
|
||||
return resultToError(result);
|
||||
}
|
||||
} else {
|
||||
const result = c.sqlite3_set_authorizer(self.handle, null, null);
|
||||
if (result != c.SQLITE_OK) {
|
||||
return resultToError(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Progress Handler
|
||||
// ========================================================================
|
||||
|
||||
/// Sets a progress handler callback.
|
||||
///
|
||||
/// The progress handler is invoked periodically during long-running queries.
|
||||
/// The `n_ops` parameter specifies how many virtual machine operations
|
||||
/// should occur between callback invocations.
|
||||
///
|
||||
/// Pass null to remove an existing progress handler.
|
||||
///
|
||||
/// Example:
|
||||
/// ```zig
|
||||
/// var should_cancel = false;
|
||||
/// fn checkCancel() bool {
|
||||
/// return !should_cancel; // return false to interrupt
|
||||
/// }
|
||||
/// try db.setProgressHandler(1000, checkCancel);
|
||||
/// ```
|
||||
pub fn setProgressHandler(self: *Self, n_ops: i32, func: ?ZigProgressFn) !void {
|
||||
if (func) |f| {
|
||||
const wrapper = try ProgressWrapper.create(f);
|
||||
c.sqlite3_progress_handler(self.handle, n_ops, progressCallback, wrapper);
|
||||
} else {
|
||||
c.sqlite3_progress_handler(self.handle, 0, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Busy Handler
|
||||
// ========================================================================
|
||||
|
||||
/// Sets a custom busy handler callback.
|
||||
///
|
||||
/// The busy handler is invoked when SQLite cannot acquire a lock.
|
||||
/// The callback receives the number of times it has been called for
|
||||
/// the current lock attempt.
|
||||
///
|
||||
/// Note: This replaces any busy timeout set with setBusyTimeout().
|
||||
///
|
||||
/// Pass null to remove an existing busy handler.
|
||||
pub fn setBusyHandler(self: *Self, func: ?ZigBusyHandlerFn) !void {
|
||||
if (func) |f| {
|
||||
const wrapper = try BusyHandlerWrapper.create(f);
|
||||
const result = c.sqlite3_busy_handler(self.handle, busyHandlerCallback, wrapper);
|
||||
if (result != c.SQLITE_OK) {
|
||||
wrapper.destroy();
|
||||
return resultToError(result);
|
||||
}
|
||||
} else {
|
||||
const result = c.sqlite3_busy_handler(self.handle, null, null);
|
||||
if (result != c.SQLITE_OK) {
|
||||
return resultToError(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Limits
|
||||
// ========================================================================
|
||||
|
||||
/// Gets the current value of a limit.
|
||||
pub fn getLimit(self: *Self, limit_type: Limit) i32 {
|
||||
return c.sqlite3_limit(self.handle, @intFromEnum(limit_type), -1);
|
||||
}
|
||||
|
||||
/// Sets a new value for a limit and returns the previous value.
|
||||
pub fn setLimit(self: *Self, limit_type: Limit, new_value: i32) i32 {
|
||||
return c.sqlite3_limit(self.handle, @intFromEnum(limit_type), new_value);
|
||||
}
|
||||
};
|
||||
|
||||
/// Flags for opening a database
|
||||
|
|
@ -943,6 +1091,48 @@ pub const Statement = struct {
|
|||
try self.bindBool(idx, value);
|
||||
}
|
||||
|
||||
/// Binds a timestamp as ISO8601 text (YYYY-MM-DD HH:MM:SS).
|
||||
///
|
||||
/// The timestamp is stored as text for SQLite date/time function compatibility.
|
||||
/// This matches the behavior of go-sqlite3 time.Time binding.
|
||||
pub fn bindTimestamp(self: *Self, index: u32, ts: i64) Error!void {
|
||||
// Format as ISO8601: YYYY-MM-DD HH:MM:SS
|
||||
const epoch_seconds = std.time.epoch.EpochSeconds{ .secs = @intCast(ts) };
|
||||
const day_seconds = epoch_seconds.getDaySeconds();
|
||||
const year_day = epoch_seconds.getEpochDay().calculateYearDay();
|
||||
const month_day = year_day.calculateMonthDay();
|
||||
|
||||
var buf: [20]u8 = undefined;
|
||||
const formatted = std.fmt.bufPrint(&buf, "{d:0>4}-{d:0>2}-{d:0>2} {d:0>2}:{d:0>2}:{d:0>2}", .{
|
||||
year_day.year,
|
||||
@intFromEnum(month_day.month),
|
||||
@as(u8, month_day.day_index) + 1, // day_index is 0-based, add 1 for display
|
||||
day_seconds.getHoursIntoDay(),
|
||||
day_seconds.getMinutesIntoHour(),
|
||||
day_seconds.getSecondsIntoMinute(),
|
||||
}) catch return Error.SqliteError;
|
||||
|
||||
try self.bindText(index, formatted);
|
||||
}
|
||||
|
||||
/// Binds a timestamp to a named parameter as ISO8601 text.
|
||||
pub fn bindTimestampNamed(self: *Self, name: [:0]const u8, ts: i64) Error!void {
|
||||
const idx = self.parameterIndex(name) orelse return Error.Range;
|
||||
try self.bindTimestamp(idx, ts);
|
||||
}
|
||||
|
||||
/// Binds the current time as ISO8601 text.
|
||||
pub fn bindCurrentTime(self: *Self, index: u32) Error!void {
|
||||
const now = std.time.timestamp();
|
||||
try self.bindTimestamp(index, now);
|
||||
}
|
||||
|
||||
/// Binds the current time to a named parameter.
|
||||
pub fn bindCurrentTimeNamed(self: *Self, name: [:0]const u8) Error!void {
|
||||
const idx = self.parameterIndex(name) orelse return Error.Range;
|
||||
try self.bindCurrentTime(idx);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Execution
|
||||
// ========================================================================
|
||||
|
|
@ -1047,6 +1237,63 @@ pub const Statement = struct {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns the database name for a column result.
|
||||
///
|
||||
/// Returns the database (e.g., "main", "temp") that the column comes from.
|
||||
/// Note: Requires SQLITE_ENABLE_COLUMN_METADATA to be defined at compile time.
|
||||
pub fn columnDatabaseName(self: *Self, index: u32) ?[]const u8 {
|
||||
const name = c.sqlite3_column_database_name(self.handle, @intCast(index));
|
||||
if (name) |n| {
|
||||
return std.mem.span(n);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns the table name for a column result.
|
||||
///
|
||||
/// Returns the original table name that the column comes from.
|
||||
/// Note: Requires SQLITE_ENABLE_COLUMN_METADATA to be defined at compile time.
|
||||
pub fn columnTableName(self: *Self, index: u32) ?[]const u8 {
|
||||
const name = c.sqlite3_column_table_name(self.handle, @intCast(index));
|
||||
if (name) |n| {
|
||||
return std.mem.span(n);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns the origin column name for a column result.
|
||||
///
|
||||
/// Returns the original column name from the table definition.
|
||||
/// Note: Requires SQLITE_ENABLE_COLUMN_METADATA to be defined at compile time.
|
||||
pub fn columnOriginName(self: *Self, index: u32) ?[]const u8 {
|
||||
const name = c.sqlite3_column_origin_name(self.handle, @intCast(index));
|
||||
if (name) |n| {
|
||||
return std.mem.span(n);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns the SQL text with bound parameters expanded.
|
||||
///
|
||||
/// Returns a string with all bound parameter values substituted into
|
||||
/// the SQL text. The caller must free the returned string using the
|
||||
/// provided allocator.
|
||||
///
|
||||
/// Returns null if out of memory or if the statement has no bound parameters.
|
||||
pub fn expandedSql(self: *Self, allocator: std.mem.Allocator) ?[]u8 {
|
||||
const expanded = c.sqlite3_expanded_sql(self.handle);
|
||||
if (expanded == null) return null;
|
||||
|
||||
const len = std.mem.len(expanded);
|
||||
const result = allocator.alloc(u8, len) catch return null;
|
||||
@memcpy(result, expanded[0..len]);
|
||||
|
||||
// Free SQLite's string
|
||||
c.sqlite3_free(expanded);
|
||||
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
|
|
@ -1860,6 +2107,283 @@ fn updateHookCallback(
|
|||
wrapper.func(op, db_str, table_str, rowid);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Authorizer
|
||||
// ============================================================================
|
||||
|
||||
/// Authorization action codes returned by the authorizer callback.
|
||||
pub const AuthAction = enum(i32) {
|
||||
create_index = c.SQLITE_CREATE_INDEX,
|
||||
create_table = c.SQLITE_CREATE_TABLE,
|
||||
create_temp_index = c.SQLITE_CREATE_TEMP_INDEX,
|
||||
create_temp_table = c.SQLITE_CREATE_TEMP_TABLE,
|
||||
create_temp_trigger = c.SQLITE_CREATE_TEMP_TRIGGER,
|
||||
create_temp_view = c.SQLITE_CREATE_TEMP_VIEW,
|
||||
create_trigger = c.SQLITE_CREATE_TRIGGER,
|
||||
create_view = c.SQLITE_CREATE_VIEW,
|
||||
delete = c.SQLITE_DELETE,
|
||||
drop_index = c.SQLITE_DROP_INDEX,
|
||||
drop_table = c.SQLITE_DROP_TABLE,
|
||||
drop_temp_index = c.SQLITE_DROP_TEMP_INDEX,
|
||||
drop_temp_table = c.SQLITE_DROP_TEMP_TABLE,
|
||||
drop_temp_trigger = c.SQLITE_DROP_TEMP_TRIGGER,
|
||||
drop_temp_view = c.SQLITE_DROP_TEMP_VIEW,
|
||||
drop_trigger = c.SQLITE_DROP_TRIGGER,
|
||||
drop_view = c.SQLITE_DROP_VIEW,
|
||||
insert = c.SQLITE_INSERT,
|
||||
pragma = c.SQLITE_PRAGMA,
|
||||
read = c.SQLITE_READ,
|
||||
select = c.SQLITE_SELECT,
|
||||
transaction = c.SQLITE_TRANSACTION,
|
||||
update = c.SQLITE_UPDATE,
|
||||
attach = c.SQLITE_ATTACH,
|
||||
detach = c.SQLITE_DETACH,
|
||||
alter_table = c.SQLITE_ALTER_TABLE,
|
||||
reindex = c.SQLITE_REINDEX,
|
||||
analyze = c.SQLITE_ANALYZE,
|
||||
create_vtable = c.SQLITE_CREATE_VTABLE,
|
||||
drop_vtable = c.SQLITE_DROP_VTABLE,
|
||||
function = c.SQLITE_FUNCTION,
|
||||
savepoint = c.SQLITE_SAVEPOINT,
|
||||
recursive = c.SQLITE_RECURSIVE,
|
||||
|
||||
pub fn fromInt(value: i32) ?AuthAction {
|
||||
inline for (@typeInfo(AuthAction).@"enum".fields) |field| {
|
||||
if (field.value == value) return @enumFromInt(value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/// Authorization return codes.
|
||||
pub const AuthResult = enum(i32) {
|
||||
ok = c.SQLITE_OK,
|
||||
deny = c.SQLITE_DENY,
|
||||
ignore = c.SQLITE_IGNORE,
|
||||
};
|
||||
|
||||
/// Zig-friendly authorizer callback type.
|
||||
/// Parameters: action, arg1 (table/index name), arg2 (column/trigger name),
|
||||
/// arg3 (database name), arg4 (trigger/view name)
|
||||
/// Returns: .ok to allow, .deny to abort with error, .ignore to treat as NULL
|
||||
pub const ZigAuthorizerFn = *const fn (
|
||||
action: AuthAction,
|
||||
arg1: ?[]const u8,
|
||||
arg2: ?[]const u8,
|
||||
arg3: ?[]const u8,
|
||||
arg4: ?[]const u8,
|
||||
) AuthResult;
|
||||
|
||||
/// Wrapper for authorizer callback.
|
||||
const AuthorizerWrapper = struct {
|
||||
func: ZigAuthorizerFn,
|
||||
|
||||
fn create(func: ZigAuthorizerFn) !*AuthorizerWrapper {
|
||||
const wrapper = try std.heap.page_allocator.create(AuthorizerWrapper);
|
||||
wrapper.func = func;
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
fn destroy(self: *AuthorizerWrapper) void {
|
||||
std.heap.page_allocator.destroy(self);
|
||||
}
|
||||
};
|
||||
|
||||
/// C callback trampoline for authorizer.
|
||||
fn authorizerCallback(
|
||||
user_data: ?*anyopaque,
|
||||
action: c_int,
|
||||
arg1: [*c]const u8,
|
||||
arg2: [*c]const u8,
|
||||
arg3: [*c]const u8,
|
||||
arg4: [*c]const u8,
|
||||
) callconv(.c) c_int {
|
||||
if (user_data == null) return c.SQLITE_OK;
|
||||
|
||||
const wrapper: *AuthorizerWrapper = @ptrCast(@alignCast(user_data));
|
||||
const auth_action = AuthAction.fromInt(action) orelse return c.SQLITE_OK;
|
||||
|
||||
const s1: ?[]const u8 = if (arg1 != null) std.mem.span(arg1) else null;
|
||||
const s2: ?[]const u8 = if (arg2 != null) std.mem.span(arg2) else null;
|
||||
const s3: ?[]const u8 = if (arg3 != null) std.mem.span(arg3) else null;
|
||||
const s4: ?[]const u8 = if (arg4 != null) std.mem.span(arg4) else null;
|
||||
|
||||
return @intFromEnum(wrapper.func(auth_action, s1, s2, s3, s4));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Progress Handler
|
||||
// ============================================================================
|
||||
|
||||
/// Zig-friendly progress handler callback type.
|
||||
/// Called periodically during long-running queries.
|
||||
/// Return true to continue, false to interrupt the query.
|
||||
pub const ZigProgressFn = *const fn () bool;
|
||||
|
||||
/// Wrapper for progress handler callback.
|
||||
const ProgressWrapper = struct {
|
||||
func: ZigProgressFn,
|
||||
|
||||
fn create(func: ZigProgressFn) !*ProgressWrapper {
|
||||
const wrapper = try std.heap.page_allocator.create(ProgressWrapper);
|
||||
wrapper.func = func;
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
fn destroy(self: *ProgressWrapper) void {
|
||||
std.heap.page_allocator.destroy(self);
|
||||
}
|
||||
};
|
||||
|
||||
/// C callback trampoline for progress handler.
|
||||
fn progressCallback(user_data: ?*anyopaque) callconv(.c) c_int {
|
||||
if (user_data == null) return 0;
|
||||
|
||||
const wrapper: *ProgressWrapper = @ptrCast(@alignCast(user_data));
|
||||
const should_continue = wrapper.func();
|
||||
return if (should_continue) 0 else 1;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Busy Handler
|
||||
// ============================================================================
|
||||
|
||||
/// Zig-friendly busy handler callback type.
|
||||
/// Called when the database is locked.
|
||||
/// Parameter: number of times the busy handler has been invoked for this lock.
|
||||
/// Return true to retry, false to return SQLITE_BUSY error.
|
||||
pub const ZigBusyHandlerFn = *const fn (count: i32) bool;
|
||||
|
||||
/// Wrapper for busy handler callback.
|
||||
const BusyHandlerWrapper = struct {
|
||||
func: ZigBusyHandlerFn,
|
||||
|
||||
fn create(func: ZigBusyHandlerFn) !*BusyHandlerWrapper {
|
||||
const wrapper = try std.heap.page_allocator.create(BusyHandlerWrapper);
|
||||
wrapper.func = func;
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
fn destroy(self: *BusyHandlerWrapper) void {
|
||||
std.heap.page_allocator.destroy(self);
|
||||
}
|
||||
};
|
||||
|
||||
/// C callback trampoline for busy handler.
|
||||
fn busyHandlerCallback(user_data: ?*anyopaque, count: c_int) callconv(.c) c_int {
|
||||
if (user_data == null) return 0;
|
||||
|
||||
const wrapper: *BusyHandlerWrapper = @ptrCast(@alignCast(user_data));
|
||||
const should_retry = wrapper.func(count);
|
||||
return if (should_retry) 1 else 0;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Limits
|
||||
// ============================================================================
|
||||
|
||||
/// SQLite limit categories.
|
||||
pub const Limit = enum(i32) {
|
||||
length = c.SQLITE_LIMIT_LENGTH,
|
||||
sql_length = c.SQLITE_LIMIT_SQL_LENGTH,
|
||||
column = c.SQLITE_LIMIT_COLUMN,
|
||||
expr_depth = c.SQLITE_LIMIT_EXPR_DEPTH,
|
||||
compound_select = c.SQLITE_LIMIT_COMPOUND_SELECT,
|
||||
vdbe_op = c.SQLITE_LIMIT_VDBE_OP,
|
||||
function_arg = c.SQLITE_LIMIT_FUNCTION_ARG,
|
||||
attached = c.SQLITE_LIMIT_ATTACHED,
|
||||
like_pattern_length = c.SQLITE_LIMIT_LIKE_PATTERN_LENGTH,
|
||||
variable_number = c.SQLITE_LIMIT_VARIABLE_NUMBER,
|
||||
trigger_depth = c.SQLITE_LIMIT_TRIGGER_DEPTH,
|
||||
worker_threads = c.SQLITE_LIMIT_WORKER_THREADS,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Pre-Update Hook
|
||||
// ============================================================================
|
||||
|
||||
/// Pre-update hook context providing access to old/new values.
|
||||
/// Only valid during the pre-update hook callback execution.
|
||||
pub const PreUpdateContext = struct {
|
||||
db: *c.sqlite3,
|
||||
|
||||
/// Returns the number of columns in the row being modified.
|
||||
pub fn columnCount(self: PreUpdateContext) i32 {
|
||||
return c.sqlite3_preupdate_count(self.db);
|
||||
}
|
||||
|
||||
/// Returns the depth of the trigger that caused the pre-update.
|
||||
/// 0 = direct operation, 1 = top-level trigger, 2 = trigger from trigger, etc.
|
||||
pub fn depth(self: PreUpdateContext) i32 {
|
||||
return c.sqlite3_preupdate_depth(self.db);
|
||||
}
|
||||
|
||||
/// Gets the old value for column N (0-indexed).
|
||||
/// Only valid for UPDATE and DELETE operations.
|
||||
pub fn oldValue(self: PreUpdateContext, col: u32) ?FunctionValue {
|
||||
var value: ?*c.sqlite3_value = null;
|
||||
const result = c.sqlite3_preupdate_old(self.db, @intCast(col), &value);
|
||||
if (result != c.SQLITE_OK or value == null) return null;
|
||||
return FunctionValue{ .value = value.? };
|
||||
}
|
||||
|
||||
/// Gets the new value for column N (0-indexed).
|
||||
/// Only valid for UPDATE and INSERT operations.
|
||||
pub fn newValue(self: PreUpdateContext, col: u32) ?FunctionValue {
|
||||
var value: ?*c.sqlite3_value = null;
|
||||
const result = c.sqlite3_preupdate_new(self.db, @intCast(col), &value);
|
||||
if (result != c.SQLITE_OK or value == null) return null;
|
||||
return FunctionValue{ .value = value.? };
|
||||
}
|
||||
};
|
||||
|
||||
/// Zig-friendly pre-update hook callback type.
|
||||
/// Parameters: context, operation, database name, table name, old rowid, new rowid
|
||||
pub const ZigPreUpdateHookFn = *const fn (
|
||||
ctx: PreUpdateContext,
|
||||
operation: UpdateOperation,
|
||||
db_name: []const u8,
|
||||
table_name: []const u8,
|
||||
old_rowid: i64,
|
||||
new_rowid: i64,
|
||||
) void;
|
||||
|
||||
/// Wrapper for pre-update hook callback.
|
||||
const PreUpdateHookWrapper = struct {
|
||||
func: ZigPreUpdateHookFn,
|
||||
|
||||
fn create(func: ZigPreUpdateHookFn) !*PreUpdateHookWrapper {
|
||||
const wrapper = try std.heap.page_allocator.create(PreUpdateHookWrapper);
|
||||
wrapper.func = func;
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
fn destroy(self: *PreUpdateHookWrapper) void {
|
||||
std.heap.page_allocator.destroy(self);
|
||||
}
|
||||
};
|
||||
|
||||
/// C callback trampoline for pre-update hook.
|
||||
fn preUpdateHookCallback(
|
||||
user_data: ?*anyopaque,
|
||||
db: ?*c.sqlite3,
|
||||
operation: c_int,
|
||||
db_name: [*c]const u8,
|
||||
table_name: [*c]const u8,
|
||||
old_rowid: c.sqlite3_int64,
|
||||
new_rowid: c.sqlite3_int64,
|
||||
) callconv(.c) void {
|
||||
if (user_data == null or db == null) return;
|
||||
|
||||
const wrapper: *PreUpdateHookWrapper = @ptrCast(@alignCast(user_data));
|
||||
const op = UpdateOperation.fromInt(operation) orelse return;
|
||||
const db_str = std.mem.span(db_name);
|
||||
const table_str = std.mem.span(table_name);
|
||||
const ctx = PreUpdateContext{ .db = db.? };
|
||||
|
||||
wrapper.func(ctx, op, db_str, table_str, old_rowid, new_rowid);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Convenience functions
|
||||
// ============================================================================
|
||||
|
|
@ -2629,3 +3153,267 @@ test "aggregate function - group concat" {
|
|||
_ = try stmt.step();
|
||||
try std.testing.expectEqualStrings("Alice,Bob,Charlie", stmt.columnText(0).?);
|
||||
}
|
||||
|
||||
// Authorizer test helper
|
||||
var auth_deny_drop_table = false;
|
||||
|
||||
fn testAuthorizer(
|
||||
action: AuthAction,
|
||||
_: ?[]const u8,
|
||||
_: ?[]const u8,
|
||||
_: ?[]const u8,
|
||||
_: ?[]const u8,
|
||||
) AuthResult {
|
||||
if (action == .drop_table and auth_deny_drop_table) {
|
||||
return .deny;
|
||||
}
|
||||
return .ok;
|
||||
}
|
||||
|
||||
test "authorizer callback" {
|
||||
var db = try openMemory();
|
||||
defer db.close();
|
||||
|
||||
try db.setAuthorizer(testAuthorizer);
|
||||
|
||||
// This should work
|
||||
try db.exec("CREATE TABLE test (x INTEGER)");
|
||||
|
||||
// Allow drop table
|
||||
auth_deny_drop_table = false;
|
||||
try db.exec("DROP TABLE test");
|
||||
|
||||
// Create again
|
||||
try db.exec("CREATE TABLE test (x INTEGER)");
|
||||
|
||||
// Now deny drop table
|
||||
auth_deny_drop_table = true;
|
||||
const result = db.exec("DROP TABLE test");
|
||||
try std.testing.expectError(Error.Auth, result);
|
||||
|
||||
// Remove authorizer
|
||||
try db.setAuthorizer(null);
|
||||
auth_deny_drop_table = false;
|
||||
}
|
||||
|
||||
// Progress handler test helper
|
||||
var progress_call_count: u32 = 0;
|
||||
|
||||
fn testProgressHandler() bool {
|
||||
progress_call_count += 1;
|
||||
return true; // Continue
|
||||
}
|
||||
|
||||
test "progress handler" {
|
||||
progress_call_count = 0;
|
||||
|
||||
var db = try openMemory();
|
||||
defer db.close();
|
||||
|
||||
try db.setProgressHandler(1, testProgressHandler);
|
||||
|
||||
// Create table and insert data to trigger some VM operations
|
||||
try db.exec("CREATE TABLE test (x INTEGER)");
|
||||
try db.exec("INSERT INTO test VALUES (1), (2), (3), (4), (5)");
|
||||
|
||||
// Progress handler should have been called multiple times
|
||||
try std.testing.expect(progress_call_count > 0);
|
||||
|
||||
// Remove handler
|
||||
try db.setProgressHandler(0, null);
|
||||
}
|
||||
|
||||
test "limits API" {
|
||||
var db = try openMemory();
|
||||
defer db.close();
|
||||
|
||||
// Get current SQL length limit
|
||||
const old_limit = db.getLimit(.sql_length);
|
||||
try std.testing.expect(old_limit > 0);
|
||||
|
||||
// Set new limit
|
||||
const prev = db.setLimit(.sql_length, 10000);
|
||||
try std.testing.expectEqual(old_limit, prev);
|
||||
|
||||
// Verify new limit
|
||||
const new_limit = db.getLimit(.sql_length);
|
||||
try std.testing.expectEqual(@as(i32, 10000), new_limit);
|
||||
|
||||
// Restore original
|
||||
_ = db.setLimit(.sql_length, old_limit);
|
||||
}
|
||||
|
||||
test "expanded SQL" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
var db = try openMemory();
|
||||
defer db.close();
|
||||
|
||||
try db.exec("CREATE TABLE users (id INTEGER, name TEXT)");
|
||||
|
||||
var stmt = try db.prepare("SELECT * FROM users WHERE id = ? AND name = ?");
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindInt(1, 42);
|
||||
try stmt.bindText(2, "Alice");
|
||||
|
||||
if (stmt.expandedSql(allocator)) |expanded| {
|
||||
defer allocator.free(expanded);
|
||||
// The expanded SQL should contain the actual values
|
||||
try std.testing.expect(std.mem.indexOf(u8, expanded, "42") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, expanded, "'Alice'") != null);
|
||||
}
|
||||
}
|
||||
|
||||
test "column metadata" {
|
||||
var db = try openMemory();
|
||||
defer db.close();
|
||||
|
||||
try db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)");
|
||||
try db.exec("INSERT INTO users VALUES (1, 'Alice', 'alice@example.com')");
|
||||
|
||||
var stmt = try db.prepare("SELECT id, name, email FROM users");
|
||||
defer stmt.finalize();
|
||||
|
||||
_ = try stmt.step();
|
||||
|
||||
// Check column database name
|
||||
if (stmt.columnDatabaseName(0)) |db_name| {
|
||||
try std.testing.expectEqualStrings("main", db_name);
|
||||
}
|
||||
|
||||
// Check column table name
|
||||
if (stmt.columnTableName(0)) |table_name| {
|
||||
try std.testing.expectEqualStrings("users", table_name);
|
||||
}
|
||||
|
||||
// Check column origin name
|
||||
if (stmt.columnOriginName(0)) |origin_name| {
|
||||
try std.testing.expectEqualStrings("id", origin_name);
|
||||
}
|
||||
if (stmt.columnOriginName(1)) |origin_name| {
|
||||
try std.testing.expectEqualStrings("name", origin_name);
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-update hook test helpers
|
||||
var preupdate_call_count: u32 = 0;
|
||||
var preupdate_old_value: ?i64 = null;
|
||||
var preupdate_new_value: ?i64 = null;
|
||||
var preupdate_op: ?UpdateOperation = null;
|
||||
|
||||
fn testPreUpdateHook(
|
||||
ctx: PreUpdateContext,
|
||||
op: UpdateOperation,
|
||||
_: []const u8, // db_name
|
||||
_: []const u8, // table_name
|
||||
_: i64, // old_rowid
|
||||
_: i64, // new_rowid
|
||||
) void {
|
||||
preupdate_call_count += 1;
|
||||
preupdate_op = op;
|
||||
|
||||
// Get old/new values depending on operation
|
||||
if (op == .update or op == .delete) {
|
||||
if (ctx.oldValue(0)) |old_val| {
|
||||
preupdate_old_value = old_val.asInt();
|
||||
}
|
||||
}
|
||||
if (op == .update or op == .insert) {
|
||||
if (ctx.newValue(0)) |new_val| {
|
||||
preupdate_new_value = new_val.asInt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test "pre-update hook" {
|
||||
preupdate_call_count = 0;
|
||||
preupdate_old_value = null;
|
||||
preupdate_new_value = null;
|
||||
preupdate_op = null;
|
||||
|
||||
var db = try openMemory();
|
||||
defer db.close();
|
||||
|
||||
try db.setPreUpdateHook(testPreUpdateHook);
|
||||
|
||||
try db.exec("CREATE TABLE test (x INTEGER)");
|
||||
|
||||
// Insert
|
||||
preupdate_call_count = 0;
|
||||
try db.exec("INSERT INTO test VALUES (10)");
|
||||
try std.testing.expect(preupdate_call_count > 0);
|
||||
try std.testing.expectEqual(UpdateOperation.insert, preupdate_op.?);
|
||||
try std.testing.expectEqual(@as(i64, 10), preupdate_new_value.?);
|
||||
|
||||
// Update
|
||||
preupdate_call_count = 0;
|
||||
preupdate_old_value = null;
|
||||
preupdate_new_value = null;
|
||||
try db.exec("UPDATE test SET x = 20 WHERE x = 10");
|
||||
try std.testing.expect(preupdate_call_count > 0);
|
||||
try std.testing.expectEqual(UpdateOperation.update, preupdate_op.?);
|
||||
try std.testing.expectEqual(@as(i64, 10), preupdate_old_value.?);
|
||||
try std.testing.expectEqual(@as(i64, 20), preupdate_new_value.?);
|
||||
|
||||
// Delete
|
||||
preupdate_call_count = 0;
|
||||
preupdate_old_value = null;
|
||||
try db.exec("DELETE FROM test WHERE x = 20");
|
||||
try std.testing.expect(preupdate_call_count > 0);
|
||||
try std.testing.expectEqual(UpdateOperation.delete, preupdate_op.?);
|
||||
try std.testing.expectEqual(@as(i64, 20), preupdate_old_value.?);
|
||||
|
||||
// Remove hook
|
||||
try db.setPreUpdateHook(null);
|
||||
}
|
||||
|
||||
test "timestamp binding" {
|
||||
var db = try openMemory();
|
||||
defer db.close();
|
||||
|
||||
try db.exec("CREATE TABLE events (id INTEGER PRIMARY KEY, name TEXT, created_at TEXT)");
|
||||
|
||||
// Insert with specific timestamp: 2024-01-15 10:30:45 UTC
|
||||
// 1705314645 is Unix timestamp for 2024-01-15 10:30:45 UTC
|
||||
var stmt = try db.prepare("INSERT INTO events (name, created_at) VALUES (?, ?)");
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindText(1, "test event");
|
||||
try stmt.bindTimestamp(2, 1705314645);
|
||||
_ = try stmt.step();
|
||||
|
||||
// Read it back
|
||||
var query = try db.prepare("SELECT created_at FROM events WHERE name = ?");
|
||||
defer query.finalize();
|
||||
|
||||
try query.bindText(1, "test event");
|
||||
const has_row = try query.step();
|
||||
try std.testing.expect(has_row);
|
||||
|
||||
const created_at = query.columnText(0) orelse "";
|
||||
try std.testing.expectEqualStrings("2024-01-15 10:30:45", created_at);
|
||||
}
|
||||
|
||||
test "timestamp binding named" {
|
||||
var db = try openMemory();
|
||||
defer db.close();
|
||||
|
||||
try db.exec("CREATE TABLE events (id INTEGER PRIMARY KEY, created_at TEXT)");
|
||||
|
||||
var stmt = try db.prepare("INSERT INTO events (created_at) VALUES (:ts)");
|
||||
defer stmt.finalize();
|
||||
|
||||
// 2020-06-15 12:00:00 UTC = 1592222400
|
||||
try stmt.bindTimestampNamed(":ts", 1592222400);
|
||||
_ = try stmt.step();
|
||||
|
||||
var query = try db.prepare("SELECT created_at FROM events");
|
||||
defer query.finalize();
|
||||
|
||||
const has_row = try query.step();
|
||||
try std.testing.expect(has_row);
|
||||
|
||||
const created_at = query.columnText(0) orelse "";
|
||||
try std.testing.expectEqualStrings("2020-06-15 12:00:00", created_at);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue