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:
reugenio 2025-12-08 18:52:18 +01:00
parent 7742f44667
commit 733533ec83
4 changed files with 1054 additions and 20 deletions

View file

@ -17,6 +17,8 @@ pub fn build(b: *std.Build) void {
"-DSQLITE_ENABLE_JSON1", // JSON functions "-DSQLITE_ENABLE_JSON1", // JSON functions
"-DSQLITE_ENABLE_RTREE", // R-Tree for geospatial "-DSQLITE_ENABLE_RTREE", // R-Tree for geospatial
"-DSQLITE_OMIT_LOAD_EXTENSION", // No dynamic extensions "-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 // zsqlite module - includes SQLite C compilation

View file

@ -1,6 +1,6 @@
# zsqlite - API Reference # zsqlite - API Reference
> **Version**: 0.4 > **Version**: 0.5
> **Ultima actualizacion**: 2025-12-08 > **Ultima actualizacion**: 2025-12-08
## Quick Reference ## Quick Reference
@ -59,6 +59,17 @@ try blob.read(&buffer, 0);
// Hooks // Hooks
try db.setCommitHook(myCommitHook); try db.setCommitHook(myCommitHook);
try db.setUpdateHook(myUpdateHook); 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 | | `interrupt()` | Interrumpe operacion |
| `isReadOnly(db_name)` | Si DB es readonly | | `isReadOnly(db_name)` | Si DB es readonly |
| `filename(db_name)` | Ruta del archivo | | `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)` | | | `bindTextNamed(name, val)` | |
| `bindBlobNamed(name, val)` | | | `bindBlobNamed(name, val)` | |
| `bindBoolNamed(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) ### Column Access (0-indexed)
@ -327,6 +357,15 @@ try db.createCollation("REVERSE", reverseOrder);
| `columnIsNull(idx)` | bool | | `columnIsNull(idx)` | bool |
| `columnBytes(idx)` | Tamano en bytes | | `columnBytes(idx)` | Tamano en bytes |
| `columnDeclType(idx)` | Tipo declarado | | `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* *2025-12-08*

View file

@ -65,7 +65,7 @@ Este documento analiza todas sus funcionalidades para asegurar paridad en zsqlit
| Bind named ($name) | ✅ | ✅ | `stmt.bindIntNamed("$name", val)` | | Bind named ($name) | ✅ | ✅ | `stmt.bindIntNamed("$name", val)` |
| Readonly check | ✅ | ✅ | `stmt.isReadOnly()` | | Readonly check | ✅ | ✅ | `stmt.isReadOnly()` |
| SQL text | ✅ | ✅ | `stmt.sql()` | | 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()` | | string/text | ✅ | ✅ | `stmt.bindText()` |
| []byte/blob | ✅ | ✅ | `stmt.bindBlob()` | | []byte/blob | ✅ | ✅ | `stmt.bindBlob()` |
| bool | ✅ | ✅ | `stmt.bindBool()` | | bool | ✅ | ✅ | `stmt.bindBool()` |
| time.Time | ✅ | ⏳ | Formatear como string ISO8601 | | time.Time | ✅ | ✅ | `stmt.bindTimestamp()` ISO8601 |
| Zeroblob | ✅ | ✅ | `stmt.bindZeroblob()` | | Zeroblob | ✅ | ✅ | `stmt.bindZeroblob()` |
--- ---
@ -98,9 +98,9 @@ Este documento analiza todas sus funcionalidades para asegurar paridad en zsqlit
| NULL check | ✅ | ✅ | `stmt.columnIsNull()` | | NULL check | ✅ | ✅ | `stmt.columnIsNull()` |
| Column bytes | ✅ | ✅ | `stmt.columnBytes()` | | Column bytes | ✅ | ✅ | `stmt.columnBytes()` |
| Declared type | ✅ | ✅ | `stmt.columnDeclType()` | | Declared type | ✅ | ✅ | `stmt.columnDeclType()` |
| Database name | ✅ | ⏳ | `sqlite3_column_database_name()` | | Database name | ✅ | ✅ | `stmt.columnDatabaseName()` |
| Table name | ✅ | ⏳ | `sqlite3_column_table_name()` | | Table name | ✅ | ✅ | `stmt.columnTableName()` |
| Origin name | ✅ | ⏳ | `sqlite3_column_origin_name()` | | Origin name | ✅ | ✅ | `stmt.columnOriginName()` |
--- ---
@ -141,8 +141,8 @@ Este documento analiza todas sus funcionalidades para asegurar paridad en zsqlit
| Funcionalidad | go-sqlite3 | zsqlite | Prioridad | | Funcionalidad | go-sqlite3 | zsqlite | Prioridad |
|---------------|------------|---------|-----------| |---------------|------------|---------|-----------|
| GetLimit | ✅ | ⏳ | Baja | | GetLimit | ✅ | ✅ | `db.getLimit()` |
| SetLimit | ✅ | ⏳ | Baja | | SetLimit | ✅ | ✅ | `db.setLimit()` |
| SetFileControlInt | ✅ | ⏳ | Baja | | SetFileControlInt | ✅ | ⏳ | Baja |
| Interrupt | ✅ | ✅ | `db.interrupt()` | | Interrupt | ✅ | ✅ | `db.interrupt()` |
@ -155,10 +155,10 @@ Este documento analiza todas sus funcionalidades para asegurar paridad en zsqlit
| Commit hook | ✅ | ✅ | `db.setCommitHook()` | | Commit hook | ✅ | ✅ | `db.setCommitHook()` |
| Rollback hook | ✅ | ✅ | `db.setRollbackHook()` | | Rollback hook | ✅ | ✅ | `db.setRollbackHook()` |
| Update hook | ✅ | ✅ | `db.setUpdateHook()` | | Update hook | ✅ | ✅ | `db.setUpdateHook()` |
| Pre-update hook | ✅ | ⏳ | Baja | | Pre-update hook | ✅ | ✅ | `db.setPreUpdateHook()` |
| Authorizer | ✅ | ⏳ | Baja | | Authorizer | ✅ | ✅ | `db.setAuthorizer()` |
| Progress handler | ✅ | ⏳ | Baja | | Progress handler | ✅ | ✅ | `db.setProgressHandler()` |
| Busy handler | ✅ | ⏳ | Media | | Busy handler | ✅ | ✅ | `db.setBusyHandler()` |
| Busy timeout | ✅ | ✅ | `db.setBusyTimeout()` | | Busy timeout | ✅ | ✅ | `db.setBusyTimeout()` |
--- ---
@ -229,13 +229,16 @@ Este documento analiza todas sus funcionalidades para asegurar paridad en zsqlit
3. ✅ Aggregator functions - `db.createAggregateFunction()` 3. ✅ Aggregator functions - `db.createAggregateFunction()`
4. ⏳ Mas pragmas 4. ⏳ Mas pragmas
### Fase 3B - Prioridad Baja (Siguiente) ### Fase 3B - Prioridad Baja ✅ COMPLETADA
1. ⏳ Authorizer 1. ✅ Authorizer - `db.setAuthorizer()`
2. ⏳ Progress handler 2. ✅ Progress handler - `db.setProgressHandler()`
3. ⏳ Pre-update hook 3. ✅ Pre-update hook - `db.setPreUpdateHook()`
4. ⏳ Window functions 4. ✅ Limits API - `db.getLimit()`, `db.setLimit()`
5. ⏳ Limits API 5. ✅ Busy handler (custom callback) - `db.setBusyHandler()`
6. ⏳ Busy handler (custom callback) 6. ✅ Timestamp binding - `stmt.bindTimestamp()`
7. ✅ Column metadata - `stmt.columnDatabaseName()`, etc.
8. ✅ Expanded SQL - `stmt.expandedSql()`
9. ⏳ Window functions (baja prioridad)
--- ---

View file

@ -24,6 +24,9 @@
const std = @import("std"); const std = @import("std");
const c = @cImport({ 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"); @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). /// Removes all hooks (commit, rollback, update).
pub fn clearHooks(self: *Self) void { pub fn clearHooks(self: *Self) void {
const commit_old = c.sqlite3_commit_hook(self.handle, null, null); const commit_old = c.sqlite3_commit_hook(self.handle, null, null);
@ -730,6 +771,113 @@ pub const Database = struct {
wrapper.destroy(); 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 /// Flags for opening a database
@ -943,6 +1091,48 @@ pub const Statement = struct {
try self.bindBool(idx, value); 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 // Execution
// ======================================================================== // ========================================================================
@ -1047,6 +1237,63 @@ pub const Statement = struct {
} }
return null; 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); 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 // Convenience functions
// ============================================================================ // ============================================================================
@ -2629,3 +3153,267 @@ test "aggregate function - group concat" {
_ = try stmt.step(); _ = try stmt.step();
try std.testing.expectEqualStrings("Alice,Bob,Charlie", stmt.columnText(0).?); 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);
}