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:
parent
532cf827f8
commit
7742f44667
4 changed files with 1077 additions and 24 deletions
38
CLAUDE.md
38
CLAUDE.md
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
> **Ultima actualizacion**: 2025-12-08
|
||||
> **Lenguaje**: Zig 0.15.2
|
||||
> **Estado**: v0.3 - Fase 2B completada
|
||||
> **Estado**: v0.4 - Fase 3A completada
|
||||
> **Inspiracion**: CGo go-sqlite3, SQLite C API
|
||||
|
||||
## Descripcion del Proyecto
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
|
||||
## Estado Actual del Proyecto
|
||||
|
||||
### Implementacion v0.3 (Fase 2B Completada)
|
||||
### Implementacion v0.4 (Fase 3A Completada)
|
||||
|
||||
| Componente | Estado | Archivo |
|
||||
|------------|--------|---------|
|
||||
|
|
@ -102,6 +102,20 @@
|
|||
| **Custom Collations** | | |
|
||||
| createCollation() | ✅ | `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** | | |
|
||||
| lastInsertRowId | ✅ | `src/root.zig` |
|
||||
| changes/totalChanges | ✅ | `src/root.zig` |
|
||||
|
|
@ -136,7 +150,10 @@
|
|||
| ATTACH/DETACH | 1 | ✅ |
|
||||
| User-defined functions | 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
|
||||
- [ ] Row iterator idiomatico
|
||||
|
||||
### Fase 3 - Avanzado (EN PROGRESO)
|
||||
- [ ] Blob streaming (para archivos grandes)
|
||||
- [ ] User-defined functions (aggregate)
|
||||
### Fase 3A - Avanzado (COMPLETADO)
|
||||
- [x] Blob streaming (para archivos grandes)
|
||||
- [x] User-defined functions (aggregate)
|
||||
- [x] Update/Commit/Rollback hooks
|
||||
|
||||
### Fase 3B - Avanzado (EN PROGRESO)
|
||||
- [ ] Authorizer callback
|
||||
- [ ] Progress handler
|
||||
- [ ] Update/Commit hooks
|
||||
- [ ] Pre-update hook
|
||||
- [ ] Window functions
|
||||
- [ ] Busy handler (custom callback)
|
||||
- [ ] Batch bind con tuples/structs
|
||||
- [ ] Row iterator idiomatico
|
||||
- [ ] Connection pooling
|
||||
|
||||
### Fase 4 - Extras
|
||||
|
|
|
|||
169
docs/API.md
169
docs/API.md
|
|
@ -1,6 +1,6 @@
|
|||
# zsqlite - API Reference
|
||||
|
||||
> **Version**: 0.3
|
||||
> **Version**: 0.4
|
||||
> **Ultima actualizacion**: 2025-12-08
|
||||
|
||||
## Quick Reference
|
||||
|
|
@ -45,9 +45,20 @@ var restored = try sqlite.loadFromFile("backup.db");
|
|||
|
||||
// User-defined functions
|
||||
try db.createScalarFunction("double", 1, myDoubleFunc);
|
||||
try db.createAggregateFunction("sum_squares", 1, stepFn, finalFn);
|
||||
|
||||
// Custom collations
|
||||
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
|
||||
```
|
||||
|
||||
### 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*
|
||||
|
|
|
|||
|
|
@ -152,9 +152,9 @@ Este documento analiza todas sus funcionalidades para asegurar paridad en zsqlit
|
|||
|
||||
| Funcionalidad | go-sqlite3 | zsqlite | Prioridad |
|
||||
|---------------|------------|---------|-----------|
|
||||
| Commit hook | ✅ | ⏳ | Media |
|
||||
| Rollback hook | ✅ | ⏳ | Media |
|
||||
| Update hook | ✅ | ⏳ | Media |
|
||||
| Commit hook | ✅ | ✅ | `db.setCommitHook()` |
|
||||
| Rollback hook | ✅ | ✅ | `db.setRollbackHook()` |
|
||||
| Update hook | ✅ | ✅ | `db.setUpdateHook()` |
|
||||
| Pre-update hook | ✅ | ⏳ | Baja |
|
||||
| Authorizer | ✅ | ⏳ | Baja |
|
||||
| Progress handler | ✅ | ⏳ | Baja |
|
||||
|
|
@ -168,7 +168,7 @@ Este documento analiza todas sus funcionalidades para asegurar paridad en zsqlit
|
|||
| Funcionalidad | go-sqlite3 | zsqlite | Prioridad |
|
||||
|---------------|------------|---------|-----------|
|
||||
| RegisterFunc (scalar) | ✅ | ✅ | `db.createScalarFunction()` |
|
||||
| RegisterAggregator | ✅ | ⏳ | Media |
|
||||
| RegisterAggregator | ✅ | ✅ | `db.createAggregateFunction()` |
|
||||
| RegisterCollation | ✅ | ✅ | `db.createCollation()` |
|
||||
| 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 |
|
||||
|---------------|------------|---------|-----------|
|
||||
| Blob open | ✅ | ⏳ | Media |
|
||||
| Blob close | ✅ | ⏳ | Media |
|
||||
| Blob read | ✅ | ⏳ | Media |
|
||||
| Blob write | ✅ | ⏳ | Media |
|
||||
| Blob bytes | ✅ | ⏳ | Media |
|
||||
| Blob reopen | ✅ | ⏳ | Baja |
|
||||
| Blob open | ✅ | ✅ | `Blob.open()` |
|
||||
| Blob close | ✅ | ✅ | `blob.close()` |
|
||||
| Blob read | ✅ | ✅ | `blob.read()` |
|
||||
| Blob write | ✅ | ✅ | `blob.write()` |
|
||||
| Blob bytes | ✅ | ✅ | `blob.bytes()` |
|
||||
| Blob reopen | ✅ | ✅ | `blob.reopen()` |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -223,18 +223,19 @@ Este documento analiza todas sus funcionalidades para asegurar paridad en zsqlit
|
|||
3. ✅ Collations personalizadas - `db.createCollation()`
|
||||
4. ✅ ATTACH/DETACH - `db.attach()`, `db.detach()`, `db.listDatabases()`
|
||||
|
||||
### Fase 3A - Prioridad Media (Siguiente)
|
||||
1. ⏳ Blob I/O streaming
|
||||
2. ⏳ Hooks (commit, rollback, update)
|
||||
3. ⏳ Aggregator functions
|
||||
### Fase 3A - Prioridad Media ✅ COMPLETADA
|
||||
1. ✅ Blob I/O streaming - `Blob` struct con `read()`, `write()`, `reopen()`
|
||||
2. ✅ Hooks (commit, rollback, update) - `db.setCommitHook()`, etc.
|
||||
3. ✅ Aggregator functions - `db.createAggregateFunction()`
|
||||
4. ⏳ Mas pragmas
|
||||
|
||||
### Fase 3B - Prioridad Baja
|
||||
### 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)
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
863
src/root.zig
863
src/root.zig
|
|
@ -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
|
||||
// ========================================================================
|
||||
|
|
@ -567,6 +626,110 @@ pub const Database = struct {
|
|||
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
|
||||
|
|
@ -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
|
||||
// ============================================================================
|
||||
|
|
@ -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
|
||||
// ============================================================================
|
||||
|
|
@ -1766,3 +2360,272 @@ test "custom collation" {
|
|||
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).?);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue