v0.4: Fase 3A complete - Blob I/O, Hooks, Aggregate Functions

New features:
- Blob I/O: Incremental read/write for large BLOBs
  - Blob.open(), close(), deinit()
  - Blob.read(), write() with offset support
  - Blob.bytes(), reopen(), readAll()
- Hooks: Monitor database changes
  - setCommitHook() - called on transaction commit
  - setRollbackHook() - called on transaction rollback
  - setUpdateHook() - called on INSERT/UPDATE/DELETE
  - clearHooks() - remove all hooks
  - UpdateOperation enum (insert, update, delete)
- Aggregate Functions: Custom multi-row aggregates
  - createAggregateFunction(name, num_args, step_fn, final_fn)
  - AggregateContext with getAggregateContext() for state management
  - Support for setNull/Int/Float/Text/Blob/Error results

Documentation:
- Updated docs/API.md to v0.4 with new features and examples
- Updated docs/CGO_PARITY_ANALYSIS.md - Fase 3A marked complete
- Updated CLAUDE.md to v0.4 with all new implementations

Tests: 28 total (8 new tests for Fase 3A features)

🤖 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:02:01 +01:00
parent 532cf827f8
commit 7742f44667
4 changed files with 1077 additions and 24 deletions

View file

@ -2,7 +2,7 @@
> **Ultima actualizacion**: 2025-12-08 > **Ultima actualizacion**: 2025-12-08
> **Lenguaje**: Zig 0.15.2 > **Lenguaje**: Zig 0.15.2
> **Estado**: v0.3 - Fase 2B completada > **Estado**: v0.4 - Fase 3A completada
> **Inspiracion**: CGo go-sqlite3, SQLite C API > **Inspiracion**: CGo go-sqlite3, SQLite C API
## Descripcion del Proyecto ## Descripcion del Proyecto
@ -22,7 +22,7 @@
## Estado Actual del Proyecto ## Estado Actual del Proyecto
### Implementacion v0.3 (Fase 2B Completada) ### Implementacion v0.4 (Fase 3A Completada)
| Componente | Estado | Archivo | | Componente | Estado | Archivo |
|------------|--------|---------| |------------|--------|---------|
@ -102,6 +102,20 @@
| **Custom Collations** | | | | **Custom Collations** | | |
| createCollation() | ✅ | `src/root.zig` | | createCollation() | ✅ | `src/root.zig` |
| removeCollation() | ✅ | `src/root.zig` | | removeCollation() | ✅ | `src/root.zig` |
| **Aggregate Functions** | | |
| createAggregateFunction() | ✅ | `src/root.zig` |
| AggregateContext | ✅ | `src/root.zig` |
| **Blob I/O** | | |
| Blob.open/openAlloc | ✅ | `src/root.zig` |
| Blob.read/write | ✅ | `src/root.zig` |
| Blob.bytes | ✅ | `src/root.zig` |
| Blob.reopen | ✅ | `src/root.zig` |
| Blob.readAll | ✅ | `src/root.zig` |
| **Hooks** | | |
| setCommitHook | ✅ | `src/root.zig` |
| setRollbackHook | ✅ | `src/root.zig` |
| setUpdateHook | ✅ | `src/root.zig` |
| clearHooks | ✅ | `src/root.zig` |
| **Utilidades** | | | | **Utilidades** | | |
| lastInsertRowId | ✅ | `src/root.zig` | | lastInsertRowId | ✅ | `src/root.zig` |
| changes/totalChanges | ✅ | `src/root.zig` | | changes/totalChanges | ✅ | `src/root.zig` |
@ -136,7 +150,10 @@
| ATTACH/DETACH | 1 | ✅ | | ATTACH/DETACH | 1 | ✅ |
| User-defined functions | 1 | ✅ | | User-defined functions | 1 | ✅ |
| Custom collations | 1 | ✅ | | Custom collations | 1 | ✅ |
| **Total** | **20** | ✅ | | Blob I/O | 3 | ✅ |
| Hooks | 3 | ✅ |
| Aggregate functions | 2 | ✅ |
| **Total** | **28** | ✅ |
--- ---
@ -171,12 +188,19 @@
- [ ] Batch bind con tuples/structs - [ ] Batch bind con tuples/structs
- [ ] Row iterator idiomatico - [ ] Row iterator idiomatico
### Fase 3 - Avanzado (EN PROGRESO) ### Fase 3A - Avanzado (COMPLETADO)
- [ ] Blob streaming (para archivos grandes) - [x] Blob streaming (para archivos grandes)
- [ ] User-defined functions (aggregate) - [x] User-defined functions (aggregate)
- [x] Update/Commit/Rollback hooks
### Fase 3B - Avanzado (EN PROGRESO)
- [ ] Authorizer callback - [ ] Authorizer callback
- [ ] Progress handler - [ ] Progress handler
- [ ] Update/Commit hooks - [ ] Pre-update hook
- [ ] Window functions
- [ ] Busy handler (custom callback)
- [ ] Batch bind con tuples/structs
- [ ] Row iterator idiomatico
- [ ] Connection pooling - [ ] Connection pooling
### Fase 4 - Extras ### Fase 4 - Extras

View file

@ -1,6 +1,6 @@
# zsqlite - API Reference # zsqlite - API Reference
> **Version**: 0.3 > **Version**: 0.4
> **Ultima actualizacion**: 2025-12-08 > **Ultima actualizacion**: 2025-12-08
## Quick Reference ## Quick Reference
@ -45,9 +45,20 @@ var restored = try sqlite.loadFromFile("backup.db");
// User-defined functions // User-defined functions
try db.createScalarFunction("double", 1, myDoubleFunc); try db.createScalarFunction("double", 1, myDoubleFunc);
try db.createAggregateFunction("sum_squares", 1, stepFn, finalFn);
// Custom collations // Custom collations
try db.createCollation("NOCASE2", myCaseInsensitiveCompare); try db.createCollation("NOCASE2", myCaseInsensitiveCompare);
// Blob I/O
var blob = try sqlite.Blob.open(&db, "main", "table", "column", rowid, true);
defer blob.deinit();
try blob.write(data, 0);
try blob.read(&buffer, 0);
// Hooks
try db.setCommitHook(myCommitHook);
try db.setUpdateHook(myUpdateHook);
``` ```
--- ---
@ -527,7 +538,161 @@ try db.createCollation("ICASE", caseInsensitive);
// SELECT * FROM users ORDER BY name COLLATE ICASE // SELECT * FROM users ORDER BY name COLLATE ICASE
``` ```
### Aggregate Function: Sum of Squares
```zig
const SumState = struct {
total: i64 = 0,
};
fn sumSquaresStep(ctx: sqlite.AggregateContext, args: []const sqlite.FunctionValue) void {
const state = ctx.getAggregateContext(SumState) orelse return;
if (args.len > 0 and !args[0].isNull()) {
const val = args[0].asInt();
state.total += val * val;
}
}
fn sumSquaresFinal(ctx: sqlite.AggregateContext) void {
const state = ctx.getAggregateContext(SumState) orelse {
ctx.setNull();
return;
};
ctx.setInt(state.total);
}
try db.createAggregateFunction("sum_squares", 1, sumSquaresStep, sumSquaresFinal);
// SELECT sum_squares(value) FROM numbers => 1+4+9+16+25 = 55
```
### Blob I/O: Incremental Read/Write
```zig
// Insert placeholder blob
try db.exec("INSERT INTO files (data) VALUES (zeroblob(1024))");
const rowid = db.lastInsertRowId();
// Open for writing
var blob = try sqlite.Blob.open(&db, "main", "files", "data", rowid, true);
defer blob.deinit();
// Write data
try blob.write("Hello, World!", 0);
// Read data
var buffer: [100]u8 = undefined;
try blob.read(&buffer, 0);
```
### Hooks: Monitor Database Changes
```zig
fn onCommit() bool {
std.debug.print("Transaction committed!\n", .{});
return true; // Allow commit
}
fn onUpdate(op: sqlite.UpdateOperation, db_name: []const u8, table: []const u8, rowid: i64) void {
std.debug.print("{s} on {s}.{s} row {d}\n", .{
@tagName(op), db_name, table, rowid
});
}
try db.setCommitHook(onCommit);
try db.setUpdateHook(onUpdate);
// ... operations will trigger hooks ...
db.clearHooks(); // Remove all hooks
```
--- ---
**© zsqlite v0.3 - API Reference** ## Blob
```zig
pub const Blob = struct {
pub fn open(db: *Database, schema: [:0]const u8, table: [:0]const u8,
column: [:0]const u8, rowid: i64, writable: bool) Error!Blob
pub fn openAlloc(db: *Database, allocator: Allocator, ...) !Blob
pub fn close(self: *Blob) Error!void
pub fn deinit(self: *Blob) void
pub fn bytes(self: *Blob) i32
pub fn read(self: *Blob, buffer: []u8, offset: i32) Error!void
pub fn write(self: *Blob, data: []const u8, offset: i32) Error!void
pub fn reopen(self: *Blob, rowid: i64) Error!void
pub fn readAll(self: *Blob, allocator: Allocator) ![]u8
};
```
---
## Hooks
### Types
```zig
pub const ZigCommitHookFn = *const fn () bool;
pub const ZigRollbackHookFn = *const fn () void;
pub const ZigUpdateHookFn = *const fn (
operation: UpdateOperation,
db_name: []const u8,
table_name: []const u8,
rowid: i64
) void;
pub const UpdateOperation = enum(i32) {
insert,
update,
delete,
};
```
### Database Methods
| Method | Description |
|--------|-------------|
| `setCommitHook(fn)` | Called when transaction commits |
| `setRollbackHook(fn)` | Called when transaction rolls back |
| `setUpdateHook(fn)` | Called on INSERT/UPDATE/DELETE |
| `clearHooks()` | Remove all hooks |
---
## Aggregate Functions
### Types
```zig
pub const AggregateStepFn = *const fn (ctx: AggregateContext, args: []const FunctionValue) void;
pub const AggregateFinalFn = *const fn (ctx: AggregateContext) void;
```
### AggregateContext
```zig
pub const AggregateContext = struct {
pub fn getAggregateContext(self: Self, comptime T: type) ?*T
pub fn setNull(self: Self) void
pub fn setInt(self: Self, value: i64) void
pub fn setFloat(self: Self, value: f64) void
pub fn setText(self: Self, value: []const u8) void
pub fn setBlob(self: Self, value: []const u8) void
pub fn setError(self: Self, msg: []const u8) void
};
```
### Database Method
```zig
pub fn createAggregateFunction(
self: *Database,
name: [:0]const u8,
num_args: i32,
step_fn: AggregateStepFn,
final_fn: AggregateFinalFn,
) !void
```
---
**© zsqlite v0.4 - API Reference**
*2025-12-08* *2025-12-08*

View file

@ -152,9 +152,9 @@ Este documento analiza todas sus funcionalidades para asegurar paridad en zsqlit
| Funcionalidad | go-sqlite3 | zsqlite | Prioridad | | Funcionalidad | go-sqlite3 | zsqlite | Prioridad |
|---------------|------------|---------|-----------| |---------------|------------|---------|-----------|
| Commit hook | ✅ | ⏳ | Media | | Commit hook | ✅ | ✅ | `db.setCommitHook()` |
| Rollback hook | ✅ | ⏳ | Media | | Rollback hook | ✅ | ✅ | `db.setRollbackHook()` |
| Update hook | ✅ | ⏳ | Media | | Update hook | ✅ | ✅ | `db.setUpdateHook()` |
| Pre-update hook | ✅ | ⏳ | Baja | | Pre-update hook | ✅ | ⏳ | Baja |
| Authorizer | ✅ | ⏳ | Baja | | Authorizer | ✅ | ⏳ | Baja |
| Progress handler | ✅ | ⏳ | Baja | | Progress handler | ✅ | ⏳ | Baja |
@ -168,7 +168,7 @@ Este documento analiza todas sus funcionalidades para asegurar paridad en zsqlit
| Funcionalidad | go-sqlite3 | zsqlite | Prioridad | | Funcionalidad | go-sqlite3 | zsqlite | Prioridad |
|---------------|------------|---------|-----------| |---------------|------------|---------|-----------|
| RegisterFunc (scalar) | ✅ | ✅ | `db.createScalarFunction()` | | RegisterFunc (scalar) | ✅ | ✅ | `db.createScalarFunction()` |
| RegisterAggregator | ✅ | ⏳ | Media | | RegisterAggregator | ✅ | ✅ | `db.createAggregateFunction()` |
| RegisterCollation | ✅ | ✅ | `db.createCollation()` | | RegisterCollation | ✅ | ✅ | `db.createCollation()` |
| User-defined window func | ✅ | ⏳ | Baja | | User-defined window func | ✅ | ⏳ | Baja |
@ -190,12 +190,12 @@ Este documento analiza todas sus funcionalidades para asegurar paridad en zsqlit
| Funcionalidad | go-sqlite3 | zsqlite | Prioridad | | Funcionalidad | go-sqlite3 | zsqlite | Prioridad |
|---------------|------------|---------|-----------| |---------------|------------|---------|-----------|
| Blob open | ✅ | ⏳ | Media | | Blob open | ✅ | ✅ | `Blob.open()` |
| Blob close | ✅ | ⏳ | Media | | Blob close | ✅ | ✅ | `blob.close()` |
| Blob read | ✅ | ⏳ | Media | | Blob read | ✅ | ✅ | `blob.read()` |
| Blob write | ✅ | ⏳ | Media | | Blob write | ✅ | ✅ | `blob.write()` |
| Blob bytes | ✅ | ⏳ | Media | | Blob bytes | ✅ | ✅ | `blob.bytes()` |
| Blob reopen | ✅ | ⏳ | Baja | | Blob reopen | ✅ | ✅ | `blob.reopen()` |
--- ---
@ -223,18 +223,19 @@ Este documento analiza todas sus funcionalidades para asegurar paridad en zsqlit
3. ✅ Collations personalizadas - `db.createCollation()` 3. ✅ Collations personalizadas - `db.createCollation()`
4. ✅ ATTACH/DETACH - `db.attach()`, `db.detach()`, `db.listDatabases()` 4. ✅ ATTACH/DETACH - `db.attach()`, `db.detach()`, `db.listDatabases()`
### Fase 3A - Prioridad Media (Siguiente) ### Fase 3A - Prioridad Media ✅ COMPLETADA
1. ⏳ Blob I/O streaming 1. ✅ Blob I/O streaming - `Blob` struct con `read()`, `write()`, `reopen()`
2. ⏳ Hooks (commit, rollback, update) 2. ✅ Hooks (commit, rollback, update) - `db.setCommitHook()`, etc.
3. ⏳ Aggregator functions 3. ✅ Aggregator functions - `db.createAggregateFunction()`
4. ⏳ Mas pragmas 4. ⏳ Mas pragmas
### Fase 3B - Prioridad Baja ### Fase 3B - Prioridad Baja (Siguiente)
1. ⏳ Authorizer 1. ⏳ Authorizer
2. ⏳ Progress handler 2. ⏳ Progress handler
3. ⏳ Pre-update hook 3. ⏳ Pre-update hook
4. ⏳ Window functions 4. ⏳ Window functions
5. ⏳ Limits API 5. ⏳ Limits API
6. ⏳ Busy handler (custom callback)
--- ---

View file

@ -512,6 +512,65 @@ pub const Database = struct {
} }
} }
/// Registers a custom aggregate function.
///
/// Aggregate functions process multiple rows and return a single result.
/// They require two callbacks:
/// - step: Called for each row, accumulates the result
/// - final: Called once at the end to produce the final result
///
/// Example (sum of squares):
/// ```zig
/// const SumSquaresState = struct {
/// total: i64 = 0,
/// };
///
/// fn sumSquaresStep(ctx: AggregateContext, args: []const FunctionValue) void {
/// const state = ctx.getAggregateContext(SumSquaresState) orelse return;
/// if (args.len > 0 and !args[0].isNull()) {
/// const val = args[0].asInt();
/// state.total += val * val;
/// }
/// }
///
/// fn sumSquaresFinal(ctx: AggregateContext) void {
/// const state = ctx.getAggregateContext(SumSquaresState) orelse {
/// ctx.setNull();
/// return;
/// };
/// ctx.setInt(state.total);
/// }
///
/// try db.createAggregateFunction("sum_squares", 1, sumSquaresStep, sumSquaresFinal);
/// // SELECT sum_squares(value) FROM numbers;
/// ```
pub fn createAggregateFunction(
self: *Self,
name: [:0]const u8,
num_args: i32,
step_fn: AggregateStepFn,
final_fn: AggregateFinalFn,
) !void {
const wrapper = try AggregateFnWrapper.create(step_fn, final_fn);
const result = c.sqlite3_create_function_v2(
self.handle,
name.ptr,
num_args,
c.SQLITE_UTF8,
wrapper,
null, // xFunc (for scalar)
aggregateStepCallback,
aggregateFinalCallback,
aggregateDestructor,
);
if (result != c.SQLITE_OK) {
wrapper.destroy();
return resultToError(result);
}
}
// ======================================================================== // ========================================================================
// Custom Collations // Custom Collations
// ======================================================================== // ========================================================================
@ -567,6 +626,110 @@ pub const Database = struct {
return resultToError(result); return resultToError(result);
} }
} }
// ========================================================================
// Hooks
// ========================================================================
/// Sets a commit hook callback.
///
/// The callback is invoked whenever a transaction is committed.
/// Return true to allow the commit, false to force a rollback.
///
/// Pass null to remove an existing hook.
///
/// Note: Only one commit hook can be active at a time.
pub fn setCommitHook(self: *Self, func: ?ZigCommitHookFn) !void {
if (func) |f| {
const wrapper = try CommitHookWrapper.create(f);
const old = c.sqlite3_commit_hook(self.handle, commitHookCallback, wrapper);
// Destroy old wrapper if it existed
if (old != null) {
const old_wrapper: *CommitHookWrapper = @ptrCast(@alignCast(old));
old_wrapper.destroy();
}
} else {
const old = c.sqlite3_commit_hook(self.handle, null, null);
if (old != null) {
const old_wrapper: *CommitHookWrapper = @ptrCast(@alignCast(old));
old_wrapper.destroy();
}
}
}
/// Sets a rollback hook callback.
///
/// The callback is invoked whenever a transaction is rolled back.
///
/// Pass null to remove an existing hook.
///
/// Note: Only one rollback hook can be active at a time.
pub fn setRollbackHook(self: *Self, func: ?ZigRollbackHookFn) !void {
if (func) |f| {
const wrapper = try RollbackHookWrapper.create(f);
const old = c.sqlite3_rollback_hook(self.handle, rollbackHookCallback, wrapper);
if (old != null) {
const old_wrapper: *RollbackHookWrapper = @ptrCast(@alignCast(old));
old_wrapper.destroy();
}
} else {
const old = c.sqlite3_rollback_hook(self.handle, null, null);
if (old != null) {
const old_wrapper: *RollbackHookWrapper = @ptrCast(@alignCast(old));
old_wrapper.destroy();
}
}
}
/// Sets an update hook callback.
///
/// The callback is invoked whenever a row is inserted, updated, or deleted.
/// The callback receives:
/// - operation: .insert, .update, or .delete
/// - db_name: Database name (e.g., "main")
/// - table_name: Table name
/// - rowid: Row ID of the affected row
///
/// Pass null to remove an existing hook.
///
/// Note: Only one update hook can be active at a time.
pub fn setUpdateHook(self: *Self, func: ?ZigUpdateHookFn) !void {
if (func) |f| {
const wrapper = try UpdateHookWrapper.create(f);
const old = c.sqlite3_update_hook(self.handle, updateHookCallback, wrapper);
if (old != null) {
const old_wrapper: *UpdateHookWrapper = @ptrCast(@alignCast(old));
old_wrapper.destroy();
}
} else {
const old = c.sqlite3_update_hook(self.handle, null, null);
if (old != null) {
const old_wrapper: *UpdateHookWrapper = @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);
if (commit_old != null) {
const wrapper: *CommitHookWrapper = @ptrCast(@alignCast(commit_old));
wrapper.destroy();
}
const rollback_old = c.sqlite3_rollback_hook(self.handle, null, null);
if (rollback_old != null) {
const wrapper: *RollbackHookWrapper = @ptrCast(@alignCast(rollback_old));
wrapper.destroy();
}
const update_old = c.sqlite3_update_hook(self.handle, null, null);
if (update_old != null) {
const wrapper: *UpdateHookWrapper = @ptrCast(@alignCast(update_old));
wrapper.destroy();
}
}
}; };
/// Flags for opening a database /// Flags for opening a database
@ -1206,6 +1369,139 @@ fn scalarDestructor(ptr: ?*anyopaque) callconv(.c) void {
} }
} }
// ============================================================================
// Aggregate Functions
// ============================================================================
/// Type signature for aggregate step function.
/// Called once for each row in the aggregate.
pub const AggregateStepFn = *const fn (ctx: AggregateContext, args: []const FunctionValue) void;
/// Type signature for aggregate final function.
/// Called once at the end to produce the result.
pub const AggregateFinalFn = *const fn (ctx: AggregateContext) void;
/// Context for aggregate functions.
///
/// Provides access to aggregate-specific memory that persists
/// across all step() calls for a single aggregate computation.
pub const AggregateContext = struct {
ctx: *c.sqlite3_context,
const Self = @This();
/// Gets the aggregate context memory.
/// The memory is initialized to zero on first call.
/// `size` is the number of bytes of context needed.
pub fn getAggregateContext(self: Self, comptime T: type) ?*T {
const ptr = c.sqlite3_aggregate_context(self.ctx, @sizeOf(T));
if (ptr == null) return null;
return @ptrCast(@alignCast(ptr));
}
/// Sets the result to NULL.
pub fn setNull(self: Self) void {
c.sqlite3_result_null(self.ctx);
}
/// Sets the result to an integer.
pub fn setInt(self: Self, value: i64) void {
c.sqlite3_result_int64(self.ctx, value);
}
/// Sets the result to a float.
pub fn setFloat(self: Self, value: f64) void {
c.sqlite3_result_double(self.ctx, value);
}
/// Sets the result to text.
pub fn setText(self: Self, value: []const u8) void {
c.sqlite3_result_text(
self.ctx,
value.ptr,
@intCast(value.len),
c.SQLITE_TRANSIENT,
);
}
/// Sets the result to a blob.
pub fn setBlob(self: Self, value: []const u8) void {
c.sqlite3_result_blob(
self.ctx,
value.ptr,
@intCast(value.len),
c.SQLITE_TRANSIENT,
);
}
/// Sets the result to an error.
pub fn setError(self: Self, msg: []const u8) void {
c.sqlite3_result_error(self.ctx, msg.ptr, @intCast(msg.len));
}
};
/// Wrapper for aggregate function pair.
const AggregateFnWrapper = struct {
step_fn: AggregateStepFn,
final_fn: AggregateFinalFn,
fn create(step_fn: AggregateStepFn, final_fn: AggregateFinalFn) !*AggregateFnWrapper {
const wrapper = try std.heap.page_allocator.create(AggregateFnWrapper);
wrapper.step_fn = step_fn;
wrapper.final_fn = final_fn;
return wrapper;
}
fn destroy(self: *AggregateFnWrapper) void {
std.heap.page_allocator.destroy(self);
}
};
/// C callback trampoline for aggregate step function.
fn aggregateStepCallback(
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: *AggregateFnWrapper = @ptrCast(@alignCast(user_data));
const agg_ctx = AggregateContext{ .ctx = ctx.? };
// Build args array
const args_count: usize = @intCast(argc);
var args: [16]FunctionValue = undefined; // Max 16 args
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 aggregate final function.
fn aggregateFinalCallback(ctx: ?*c.sqlite3_context) callconv(.c) void {
const user_data = c.sqlite3_user_data(ctx);
if (user_data == null) return;
const wrapper: *AggregateFnWrapper = @ptrCast(@alignCast(user_data));
const agg_ctx = AggregateContext{ .ctx = ctx.? };
wrapper.final_fn(agg_ctx);
}
/// Destructor callback for aggregate function user data.
fn aggregateDestructor(ptr: ?*anyopaque) callconv(.c) void {
if (ptr) |p| {
const wrapper: *AggregateFnWrapper = @ptrCast(@alignCast(p));
wrapper.destroy();
}
}
// ============================================================================ // ============================================================================
// Custom Collations // Custom Collations
// ============================================================================ // ============================================================================
@ -1266,6 +1562,304 @@ fn collationDestructor(ptr: ?*anyopaque) callconv(.c) void {
} }
} }
// ============================================================================
// Blob I/O
// ============================================================================
/// Blob handle for incremental I/O operations.
///
/// Allows reading and writing large BLOBs incrementally without
/// loading the entire blob into memory.
pub const Blob = struct {
handle: ?*c.sqlite3_blob,
db: *Database,
const Self = @This();
/// Opens a blob for incremental I/O.
///
/// Parameters:
/// - `db`: Database connection
/// - `schema`: Database name (usually "main")
/// - `table`: Table name
/// - `column`: Column name
/// - `rowid`: Row ID of the blob
/// - `writable`: If true, opens for read/write; otherwise read-only
pub fn open(
db: *Database,
schema: [:0]const u8,
table: [:0]const u8,
column: [:0]const u8,
rowid: i64,
writable: bool,
) Error!Self {
var handle: ?*c.sqlite3_blob = null;
const flags: c_int = if (writable) 1 else 0;
const result = c.sqlite3_blob_open(
db.handle,
schema.ptr,
table.ptr,
column.ptr,
rowid,
flags,
&handle,
);
if (result != c.SQLITE_OK) {
return resultToError(result);
}
return Self{
.handle = handle,
.db = db,
};
}
/// Opens a blob with allocator for runtime strings.
pub fn openAlloc(
db: *Database,
allocator: std.mem.Allocator,
schema: []const u8,
table: []const u8,
column: []const u8,
rowid: i64,
writable: bool,
) !Self {
// Create null-terminated copies
const schema_z = try allocator.dupeZ(u8, schema);
defer allocator.free(schema_z);
const table_z = try allocator.dupeZ(u8, table);
defer allocator.free(table_z);
const column_z = try allocator.dupeZ(u8, column);
defer allocator.free(column_z);
return Self.open(db, schema_z, table_z, column_z, rowid, writable);
}
/// Closes the blob handle.
pub fn close(self: *Self) Error!void {
if (self.handle) |h| {
const result = c.sqlite3_blob_close(h);
self.handle = null;
if (result != c.SQLITE_OK) {
return resultToError(result);
}
}
}
/// Closes the blob handle without checking for errors.
pub fn deinit(self: *Self) void {
if (self.handle) |h| {
_ = c.sqlite3_blob_close(h);
self.handle = null;
}
}
/// Returns the size of the blob in bytes.
pub fn bytes(self: *Self) i32 {
if (self.handle) |h| {
return c.sqlite3_blob_bytes(h);
}
return 0;
}
/// Reads data from the blob.
///
/// Parameters:
/// - `buffer`: Buffer to read into
/// - `offset`: Offset within the blob to start reading
pub fn read(self: *Self, buffer: []u8, offset: i32) Error!void {
if (self.handle == null) return error.SqliteError;
const result = c.sqlite3_blob_read(
self.handle,
buffer.ptr,
@intCast(buffer.len),
offset,
);
if (result != c.SQLITE_OK) {
return resultToError(result);
}
}
/// Writes data to the blob.
///
/// Parameters:
/// - `data`: Data to write
/// - `offset`: Offset within the blob to start writing
///
/// Note: This cannot change the size of the blob. Use UPDATE to resize.
pub fn write(self: *Self, data: []const u8, offset: i32) Error!void {
if (self.handle == null) return error.SqliteError;
const result = c.sqlite3_blob_write(
self.handle,
data.ptr,
@intCast(data.len),
offset,
);
if (result != c.SQLITE_OK) {
return resultToError(result);
}
}
/// Moves the blob handle to a different row.
///
/// This allows reusing an open blob handle for a different row
/// in the same table, which is faster than closing and reopening.
pub fn reopen(self: *Self, rowid: i64) Error!void {
if (self.handle == null) return error.SqliteError;
const result = c.sqlite3_blob_reopen(self.handle, rowid);
if (result != c.SQLITE_OK) {
return resultToError(result);
}
}
/// Reads the entire blob into a newly allocated buffer.
pub fn readAll(self: *Self, allocator: std.mem.Allocator) ![]u8 {
const size = self.bytes();
if (size <= 0) return &[_]u8{};
const buffer = try allocator.alloc(u8, @intCast(size));
errdefer allocator.free(buffer);
try self.read(buffer, 0);
return buffer;
}
};
// ============================================================================
// Hooks (Commit, Rollback, Update)
// ============================================================================
/// Type signature for commit hook callback.
/// Return 0 to allow the commit, non-zero to force rollback.
pub const CommitHookFn = *const fn (user_data: ?*anyopaque) i32;
/// Type signature for rollback hook callback.
pub const RollbackHookFn = *const fn (user_data: ?*anyopaque) void;
/// Type signature for update hook callback.
/// Parameters are: operation (INSERT/UPDATE/DELETE), database name, table name, rowid
pub const UpdateHookFn = *const fn (
user_data: ?*anyopaque,
operation: i32,
db_name: [*:0]const u8,
table_name: [*:0]const u8,
rowid: i64,
) void;
/// Update operation types for update hooks.
pub const UpdateOperation = enum(i32) {
insert = c.SQLITE_INSERT,
update = c.SQLITE_UPDATE,
delete = c.SQLITE_DELETE,
pub fn fromInt(value: i32) ?UpdateOperation {
return switch (value) {
c.SQLITE_INSERT => .insert,
c.SQLITE_UPDATE => .update,
c.SQLITE_DELETE => .delete,
else => null,
};
}
};
/// Zig-friendly commit hook callback type.
pub const ZigCommitHookFn = *const fn () bool;
/// Zig-friendly rollback hook callback type.
pub const ZigRollbackHookFn = *const fn () void;
/// Zig-friendly update hook callback type.
pub const ZigUpdateHookFn = *const fn (operation: UpdateOperation, db_name: []const u8, table_name: []const u8, rowid: i64) void;
/// Wrapper for Zig commit hook.
const CommitHookWrapper = struct {
func: ZigCommitHookFn,
fn create(func: ZigCommitHookFn) !*CommitHookWrapper {
const wrapper = try std.heap.page_allocator.create(CommitHookWrapper);
wrapper.func = func;
return wrapper;
}
fn destroy(self: *CommitHookWrapper) void {
std.heap.page_allocator.destroy(self);
}
};
/// Wrapper for Zig rollback hook.
const RollbackHookWrapper = struct {
func: ZigRollbackHookFn,
fn create(func: ZigRollbackHookFn) !*RollbackHookWrapper {
const wrapper = try std.heap.page_allocator.create(RollbackHookWrapper);
wrapper.func = func;
return wrapper;
}
fn destroy(self: *RollbackHookWrapper) void {
std.heap.page_allocator.destroy(self);
}
};
/// Wrapper for Zig update hook.
const UpdateHookWrapper = struct {
func: ZigUpdateHookFn,
fn create(func: ZigUpdateHookFn) !*UpdateHookWrapper {
const wrapper = try std.heap.page_allocator.create(UpdateHookWrapper);
wrapper.func = func;
return wrapper;
}
fn destroy(self: *UpdateHookWrapper) void {
std.heap.page_allocator.destroy(self);
}
};
/// C callback trampoline for commit hooks.
fn commitHookCallback(user_data: ?*anyopaque) callconv(.c) c_int {
if (user_data == null) return 0;
const wrapper: *CommitHookWrapper = @ptrCast(@alignCast(user_data));
const allow_commit = wrapper.func();
return if (allow_commit) 0 else 1;
}
/// C callback trampoline for rollback hooks.
fn rollbackHookCallback(user_data: ?*anyopaque) callconv(.c) void {
if (user_data == null) return;
const wrapper: *RollbackHookWrapper = @ptrCast(@alignCast(user_data));
wrapper.func();
}
/// C callback trampoline for update hooks.
fn updateHookCallback(
user_data: ?*anyopaque,
operation: c_int,
db_name: [*c]const u8,
table_name: [*c]const u8,
rowid: c.sqlite3_int64,
) callconv(.c) void {
if (user_data == null) return;
const wrapper: *UpdateHookWrapper = @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);
wrapper.func(op, db_str, table_str, rowid);
}
// ============================================================================ // ============================================================================
// Convenience functions // Convenience functions
// ============================================================================ // ============================================================================
@ -1766,3 +2360,272 @@ test "custom collation" {
try std.testing.expectEqualStrings("Alice", stmt.columnText(0).?); try std.testing.expectEqualStrings("Alice", stmt.columnText(0).?);
} }
} }
test "blob incremental I/O" {
var db = try openMemory();
defer db.close();
// Create table with blob column
try db.exec("CREATE TABLE blobs (id INTEGER PRIMARY KEY, data BLOB)");
// Insert a blob with zeroblob placeholder
var insert = try db.prepare("INSERT INTO blobs (data) VALUES (zeroblob(100))");
defer insert.finalize();
_ = try insert.step();
const rowid = db.lastInsertRowId();
// Open blob for writing
var blob = try Blob.open(&db, "main", "blobs", "data", rowid, true);
defer blob.deinit();
// Check size
try std.testing.expectEqual(@as(i32, 100), blob.bytes());
// Write some data
const write_data = "Hello, Blob World!";
try blob.write(write_data, 0);
// Close and reopen for reading
try blob.close();
blob = try Blob.open(&db, "main", "blobs", "data", rowid, false);
// Read back
var read_buffer: [100]u8 = undefined;
try blob.read(&read_buffer, 0);
try std.testing.expectEqualStrings(write_data, read_buffer[0..write_data.len]);
}
test "blob read all" {
const allocator = std.testing.allocator;
var db = try openMemory();
defer db.close();
try db.exec("CREATE TABLE files (id INTEGER PRIMARY KEY, content BLOB)");
// Insert test data
var insert = try db.prepare("INSERT INTO files (content) VALUES (?)");
defer insert.finalize();
const test_data = "This is test blob content for readAll test";
try insert.bindBlob(1, test_data);
_ = try insert.step();
const rowid = db.lastInsertRowId();
// Read using blob API
var blob = try Blob.open(&db, "main", "files", "content", rowid, false);
defer blob.deinit();
const data = try blob.readAll(allocator);
defer allocator.free(data);
try std.testing.expectEqualStrings(test_data, data);
}
test "blob reopen" {
var db = try openMemory();
defer db.close();
try db.exec("CREATE TABLE multi (id INTEGER PRIMARY KEY, data BLOB)");
// Insert multiple rows
try db.exec("INSERT INTO multi (data) VALUES (X'0102030405')"); // rowid 1
try db.exec("INSERT INTO multi (data) VALUES (X'0A0B0C0D0E')"); // rowid 2
// Open first row
var blob = try Blob.open(&db, "main", "multi", "data", 1, false);
defer blob.deinit();
var buffer: [5]u8 = undefined;
try blob.read(&buffer, 0);
try std.testing.expectEqualSlices(u8, &[_]u8{ 1, 2, 3, 4, 5 }, &buffer);
// Reopen to second row
try blob.reopen(2);
try blob.read(&buffer, 0);
try std.testing.expectEqualSlices(u8, &[_]u8{ 10, 11, 12, 13, 14 }, &buffer);
}
// Thread-local state for hook tests
var commit_count: u32 = 0;
var rollback_count: u32 = 0;
var update_count: u32 = 0;
var last_update_op: ?UpdateOperation = null;
fn testCommitHook() bool {
commit_count += 1;
return true; // Allow commit
}
fn testRollbackHook() void {
rollback_count += 1;
}
fn testUpdateHook(op: UpdateOperation, _: []const u8, _: []const u8, _: i64) void {
update_count += 1;
last_update_op = op;
}
test "commit hook" {
commit_count = 0;
var db = try openMemory();
defer db.close();
try db.setCommitHook(testCommitHook);
try db.exec("CREATE TABLE test (x INTEGER)");
try std.testing.expect(commit_count >= 1);
const before = commit_count;
try db.begin();
try db.exec("INSERT INTO test VALUES (1)");
try db.commit();
try std.testing.expect(commit_count > before);
// Remove hook
try db.setCommitHook(null);
}
test "rollback hook" {
rollback_count = 0;
var db = try openMemory();
defer db.close();
try db.setRollbackHook(testRollbackHook);
try db.exec("CREATE TABLE test (x INTEGER)");
try db.begin();
try db.exec("INSERT INTO test VALUES (1)");
try db.rollback();
try std.testing.expect(rollback_count >= 1);
// Remove hook
try db.setRollbackHook(null);
}
test "update hook" {
update_count = 0;
last_update_op = null;
var db = try openMemory();
defer db.close();
try db.setUpdateHook(testUpdateHook);
try db.exec("CREATE TABLE test (x INTEGER)");
// Insert
try db.exec("INSERT INTO test VALUES (1)");
try std.testing.expect(last_update_op == .insert);
// Update
try db.exec("UPDATE test SET x = 2 WHERE x = 1");
try std.testing.expect(last_update_op == .update);
// Delete
try db.exec("DELETE FROM test WHERE x = 2");
try std.testing.expect(last_update_op == .delete);
try std.testing.expect(update_count >= 3);
// Clear all hooks
db.clearHooks();
}
// Aggregate function state for sum of squares
const SumSquaresState = struct {
total: i64 = 0,
};
fn sumSquaresStep(ctx: AggregateContext, args: []const FunctionValue) void {
const state = ctx.getAggregateContext(SumSquaresState) orelse return;
if (args.len > 0 and !args[0].isNull()) {
const val = args[0].asInt();
state.total += val * val;
}
}
fn sumSquaresFinal(ctx: AggregateContext) void {
const state = ctx.getAggregateContext(SumSquaresState) orelse {
ctx.setNull();
return;
};
ctx.setInt(state.total);
}
test "aggregate function - sum of squares" {
var db = try openMemory();
defer db.close();
try db.createAggregateFunction("sum_squares", 1, sumSquaresStep, sumSquaresFinal);
try db.exec("CREATE TABLE numbers (value INTEGER)");
try db.exec("INSERT INTO numbers VALUES (1), (2), (3), (4), (5)");
// sum of squares: 1 + 4 + 9 + 16 + 25 = 55
var stmt = try db.prepare("SELECT sum_squares(value) FROM numbers");
defer stmt.finalize();
_ = try stmt.step();
try std.testing.expectEqual(@as(i64, 55), stmt.columnInt(0));
}
// Aggregate function for string concatenation
const ConcatState = struct {
buffer: [256]u8 = undefined,
len: usize = 0,
separator_seen: bool = false,
};
fn groupConcatStep(ctx: AggregateContext, args: []const FunctionValue) void {
const state = ctx.getAggregateContext(ConcatState) orelse return;
if (args.len > 0) {
if (args[0].asText()) |text| {
// Add separator if not first
if (state.separator_seen and state.len < state.buffer.len - 1) {
state.buffer[state.len] = ',';
state.len += 1;
}
// Copy text
const remaining = state.buffer.len - state.len;
const copy_len = @min(text.len, remaining);
@memcpy(state.buffer[state.len..][0..copy_len], text[0..copy_len]);
state.len += copy_len;
state.separator_seen = true;
}
}
}
fn groupConcatFinal(ctx: AggregateContext) void {
const state = ctx.getAggregateContext(ConcatState) orelse {
ctx.setNull();
return;
};
if (state.len == 0) {
ctx.setNull();
} else {
ctx.setText(state.buffer[0..state.len]);
}
}
test "aggregate function - group concat" {
var db = try openMemory();
defer db.close();
try db.createAggregateFunction("my_group_concat", 1, groupConcatStep, groupConcatFinal);
try db.exec("CREATE TABLE names (name TEXT)");
try db.exec("INSERT INTO names VALUES ('Alice'), ('Bob'), ('Charlie')");
var stmt = try db.prepare("SELECT my_group_concat(name) FROM names");
defer stmt.finalize();
_ = try stmt.step();
try std.testing.expectEqualStrings("Alice,Bob,Charlie", stmt.columnText(0).?);
}