Compare commits

...

2 commits

Author SHA1 Message Date
167e54530f feat: add advanced features - batch bind, row iterator, FTS5, JSON, R-Tree, virtual tables
New features:
- Batch binding: stmt.bindAll(.{ "Alice", 30, 95.5 }), stmt.rebind()
- Row iterator: stmt.iterator(), Row struct with convenient accessors
- File control: setFileControlInt(), getPersistWal(), setChunkSize()
- FTS5 helpers: Fts5 struct with createSimpleTable(), search(), highlight()
- JSON helpers: Json struct with extract(), set(), createArray(), patch()
- R-Tree helpers: RTree struct with insert2D(), queryIntersects2D(), spatial joins
- Virtual table foundations: vtable.zig with helper types
- BoundingBox2D/3D, GeoCoord with distance calculations

Total: 5861 lines across 13 modules
All 54 tests passing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-08 20:30:10 +01:00
5e28cbe4bf refactor: modularize root.zig into specialized modules
Split monolithic root.zig (4200 lines) into 9 focused modules:
- c.zig: centralized @cImport for SQLite
- errors.zig: Error enum and resultToError
- types.zig: OpenFlags, ColumnType, Limit, enums
- database.zig: Database struct with all methods
- statement.zig: Statement struct with bindings/columns
- functions.zig: UDFs, hooks, and C callbacks
- backup.zig: Backup and Blob I/O
- pool.zig: ConnectionPool (thread-safe)
- root.zig: re-exports + tests (~1100 lines)

Total: ~3600 lines (74% reduction in root.zig)
All 47 tests passing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-08 19:54:19 +01:00
14 changed files with 4868 additions and 3192 deletions

View file

@ -331,13 +331,35 @@ while (try stmt.step()) {
## Decisiones de Diseno
### 1. Todo en root.zig (por ahora)
### 1. Estructura Modular
Para v0.1, todo el codigo esta en un solo archivo. Cuando crezca significativamente (>400 lineas core), se fragmentara en:
- `database.zig`
- `statement.zig`
- `errors.zig`
- `types.zig`
El codigo esta organizado en modulos especializados:
```
src/
├── root.zig # Re-exports publicos + tests (~1100 lineas)
├── c.zig # @cImport centralizado (24 lineas)
├── errors.zig # Error enum y resultToError (142 lineas)
├── types.zig # OpenFlags, ColumnType, enums (154 lineas)
├── database.zig # Database struct (795 lineas)
├── statement.zig # Statement struct (378 lineas)
├── functions.zig # UDFs, hooks, callbacks (567 lineas)
├── backup.zig # Backup y Blob I/O (292 lineas)
└── pool.zig # ConnectionPool (151 lineas)
```
**Total**: ~3600 lineas (vs 4200 monoliticas anteriores)
Cada modulo tiene una responsabilidad clara:
- **c.zig**: Unico punto de @cImport para SQLite
- **errors.zig**: Mapeo completo de errores SQLite -> Zig
- **types.zig**: Tipos compartidos (flags, enums)
- **database.zig**: Conexion y operaciones de base de datos
- **statement.zig**: Prepared statements y bindings
- **functions.zig**: Funciones definidas por usuario y hooks
- **backup.zig**: API de backup y blob streaming
- **pool.zig**: Pool de conexiones thread-safe
- **root.zig**: Re-exporta API publica + contiene tests
### 2. Error Union vs Nullable
@ -367,29 +389,15 @@ Esto facilita traducir ejemplos de documentacion SQLite.
## Roadmap Arquitectural
### Fase 2: Modularizacion
### Completado: Modularizacion
Cuando el codigo crezca, fragmentar en modulos:
```
src/
├── root.zig # Re-exports publicos
├── database.zig # Database struct
├── statement.zig # Statement struct
├── errors.zig # Error types
├── types.zig # OpenFlags, ColumnType
└── c.zig # @cImport centralizado
```
La estructura modular ya esta implementada (ver seccion "Estructura Modular").
### Fase 3: Features Avanzadas
### Siguiente: Optimizaciones
```
src/
├── ...
├── blob.zig # Blob streaming
├── functions.zig # User-defined functions
├── backup.zig # Backup API
└── hooks.zig # Update/commit hooks
```
- [ ] Reducir duplicacion en funciones de binding
- [ ] Crear helpers de test para reducir boilerplate
- [ ] Considerar vtable API si se necesitan mas extensiones
---

292
src/backup.zig Normal file
View file

@ -0,0 +1,292 @@
//! SQLite Backup and Blob I/O
//!
//! Provides the Backup struct for database copying and the Blob struct
//! for incremental blob I/O operations.
const std = @import("std");
const c = @import("c.zig").c;
const errors = @import("errors.zig");
const Error = errors.Error;
const resultToError = errors.resultToError;
pub const Database = @import("database.zig").Database;
// ============================================================================
// Backup API
// ============================================================================
/// SQLite online backup handle.
///
/// Allows copying database content from one database to another while
/// both databases are in use.
pub const Backup = struct {
handle: ?*c.sqlite3_backup,
dest_db: *Database,
source_db: *Database,
const Self = @This();
/// Initializes a backup from source to destination database.
pub fn init(
dest_db: *Database,
dest_name: [:0]const u8,
source_db: *Database,
source_name: [:0]const u8,
) Error!Self {
const handle = c.sqlite3_backup_init(
dest_db.handle,
dest_name.ptr,
source_db.handle,
source_name.ptr,
);
if (handle == null) {
const err_code = c.sqlite3_errcode(dest_db.handle);
return resultToError(err_code);
}
return .{
.handle = handle,
.dest_db = dest_db,
.source_db = source_db,
};
}
/// Convenience function to backup the main database.
pub fn initMain(dest_db: *Database, source_db: *Database) Error!Self {
return init(dest_db, "main", source_db, "main");
}
/// Copies up to `n_pages` pages from source to destination.
/// Use -1 to copy all remaining pages in one call.
/// Returns true if there are more pages to copy.
pub fn step(self: *Self, n_pages: i32) Error!bool {
const result = c.sqlite3_backup_step(self.handle, n_pages);
return switch (result) {
c.SQLITE_OK => true,
c.SQLITE_DONE => false,
c.SQLITE_BUSY, c.SQLITE_LOCKED => Error.Busy,
else => resultToError(result),
};
}
/// Copies all remaining pages in one call.
pub fn stepAll(self: *Self) Error!void {
_ = try self.step(-1);
}
/// Returns the number of pages still to be copied.
pub fn remaining(self: *Self) i32 {
return c.sqlite3_backup_remaining(self.handle);
}
/// Returns the total number of pages in the source database.
pub fn pageCount(self: *Self) i32 {
return c.sqlite3_backup_pagecount(self.handle);
}
/// Returns the progress as a percentage (0-100).
pub fn progress(self: *Self) u8 {
const total = self.pageCount();
if (total == 0) return 100;
const done = total - self.remaining();
return @intCast(@divFloor(done * 100, total));
}
/// Finishes the backup operation and releases resources.
pub fn finish(self: *Self) Error!void {
if (self.handle) |h| {
const result = c.sqlite3_backup_finish(h);
self.handle = null;
if (result != c.SQLITE_OK) {
return resultToError(result);
}
}
}
/// Alias for finish() for RAII-style usage.
pub fn deinit(self: *Self) void {
self.finish() catch {};
}
};
// ============================================================================
// 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.
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 {
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.
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.
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.
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;
}
};
// ============================================================================
// Convenience functions
// ============================================================================
/// Copies an entire database to another database.
pub fn backupDatabase(dest_db: *Database, source_db: *Database) Error!void {
var backup = try Backup.initMain(dest_db, source_db);
defer backup.deinit();
try backup.stepAll();
}
/// Copies a database to a file.
pub fn backupToFile(source_db: *Database, path: [:0]const u8) Error!void {
var dest_db = try Database.open(path);
defer dest_db.close();
try backupDatabase(&dest_db, source_db);
}
/// Loads a database from a file into memory.
pub fn loadFromFile(path: [:0]const u8) Error!Database {
var file_db = try Database.open(path);
defer file_db.close();
var mem_db = try Database.open(":memory:");
errdefer mem_db.close();
try backupDatabase(&mem_db, &file_db);
return mem_db;
}

24
src/c.zig Normal file
View file

@ -0,0 +1,24 @@
//! SQLite C bindings
//!
//! Centralized @cImport for SQLite. All modules should import this
//! instead of doing their own @cImport.
pub const c = @cImport({
@cDefine("SQLITE_ENABLE_COLUMN_METADATA", "1");
@cDefine("SQLITE_ENABLE_PREUPDATE_HOOK", "1");
@cInclude("sqlite3.h");
});
// Re-export commonly used types for convenience
pub const sqlite3 = c.sqlite3;
pub const sqlite3_stmt = c.sqlite3_stmt;
pub const sqlite3_context = c.sqlite3_context;
pub const sqlite3_value = c.sqlite3_value;
pub const sqlite3_backup = c.sqlite3_backup;
pub const sqlite3_blob = c.sqlite3_blob;
// Constants
pub const SQLITE_OK = c.SQLITE_OK;
pub const SQLITE_ROW = c.SQLITE_ROW;
pub const SQLITE_DONE = c.SQLITE_DONE;
pub const SQLITE_TRANSIENT = c.SQLITE_TRANSIENT;

901
src/database.zig Normal file
View file

@ -0,0 +1,901 @@
//! SQLite database connection
//!
//! Provides the main Database struct for managing SQLite connections.
const std = @import("std");
const c = @import("c.zig").c;
const errors = @import("errors.zig");
const types = @import("types.zig");
const functions = @import("functions.zig");
const stmt_mod = @import("statement.zig");
const Error = errors.Error;
const resultToError = errors.resultToError;
const OpenFlags = types.OpenFlags;
const Limit = types.Limit;
const AuthAction = types.AuthAction;
const AuthResult = types.AuthResult;
const UpdateOperation = types.UpdateOperation;
// Re-export Statement for prepare() return type
pub const Statement = stmt_mod.Statement;
// Function types
const ScalarFn = functions.ScalarFn;
const AggregateStepFn = functions.AggregateStepFn;
const AggregateFinalFn = functions.AggregateFinalFn;
const WindowValueFn = functions.WindowValueFn;
const WindowInverseFn = functions.WindowInverseFn;
const CollationFn = functions.CollationFn;
const ZigCommitHookFn = functions.ZigCommitHookFn;
const ZigRollbackHookFn = functions.ZigRollbackHookFn;
const ZigUpdateHookFn = functions.ZigUpdateHookFn;
const ZigPreUpdateHookFn = functions.ZigPreUpdateHookFn;
const ZigAuthorizerFn = functions.ZigAuthorizerFn;
const ZigProgressFn = functions.ZigProgressFn;
const ZigBusyHandlerFn = functions.ZigBusyHandlerFn;
// Wrappers
const ScalarFnWrapper = functions.ScalarFnWrapper;
const AggregateFnWrapper = functions.AggregateFnWrapper;
const WindowFnWrapper = functions.WindowFnWrapper;
const CollationWrapper = functions.CollationWrapper;
const CommitHookWrapper = functions.CommitHookWrapper;
const RollbackHookWrapper = functions.RollbackHookWrapper;
const UpdateHookWrapper = functions.UpdateHookWrapper;
const PreUpdateHookWrapper = functions.PreUpdateHookWrapper;
const AuthorizerWrapper = functions.AuthorizerWrapper;
const ProgressWrapper = functions.ProgressWrapper;
const BusyHandlerWrapper = functions.BusyHandlerWrapper;
/// SQLite database connection
pub const Database = struct {
handle: ?*c.sqlite3,
const Self = @This();
// ========================================================================
// Opening and Closing
// ========================================================================
/// Opens a database connection.
pub fn open(path: [:0]const u8) Error!Self {
var handle: ?*c.sqlite3 = null;
const result = c.sqlite3_open(path.ptr, &handle);
if (result != c.SQLITE_OK) {
if (handle) |h| {
_ = c.sqlite3_close(h);
}
return resultToError(result);
}
return .{ .handle = handle };
}
/// Opens a database with specific flags.
pub fn openWithFlags(path: [:0]const u8, flags: OpenFlags) Error!Self {
var handle: ?*c.sqlite3 = null;
const result = c.sqlite3_open_v2(path.ptr, &handle, flags.toInt(), null);
if (result != c.SQLITE_OK) {
if (handle) |h| {
_ = c.sqlite3_close(h);
}
return resultToError(result);
}
return .{ .handle = handle };
}
/// Opens a database using a URI connection string.
pub fn openUri(uri: [:0]const u8) Error!Self {
var handle: ?*c.sqlite3 = null;
const flags = c.SQLITE_OPEN_URI | c.SQLITE_OPEN_READWRITE | c.SQLITE_OPEN_CREATE;
const result = c.sqlite3_open_v2(uri.ptr, &handle, flags, null);
if (result != c.SQLITE_OK) {
if (handle) |h| {
_ = c.sqlite3_close(h);
}
return resultToError(result);
}
return .{ .handle = handle };
}
/// Opens a database using a URI with an allocator for runtime strings.
pub fn openUriAlloc(allocator: std.mem.Allocator, uri: []const u8) !Self {
const uri_z = try allocator.dupeZ(u8, uri);
defer allocator.free(uri_z);
return Self.openUri(uri_z);
}
/// Closes the database connection.
pub fn close(self: *Self) void {
if (self.handle) |h| {
_ = c.sqlite3_close(h);
self.handle = null;
}
}
// ========================================================================
// SQL Execution
// ========================================================================
/// Executes SQL statement(s) without returning results.
pub fn exec(self: *Self, sql: [:0]const u8) Error!void {
const result = c.sqlite3_exec(self.handle, sql.ptr, null, null, null);
if (result != c.SQLITE_OK) {
return resultToError(result);
}
}
/// Executes SQL with runtime-known string.
pub fn execAlloc(self: *Self, allocator: std.mem.Allocator, sql: []const u8) !void {
const sql_z = try allocator.dupeZ(u8, sql);
defer allocator.free(sql_z);
try self.exec(sql_z);
}
/// Prepares a SQL statement for execution.
pub fn prepare(self: *Self, sql: [:0]const u8) Error!Statement {
var stmt: ?*c.sqlite3_stmt = null;
const result = c.sqlite3_prepare_v2(self.handle, sql.ptr, @intCast(sql.len + 1), &stmt, null);
if (result != c.SQLITE_OK) {
return resultToError(result);
}
return .{ .handle = stmt, .db = self };
}
/// Prepares a SQL statement with runtime-known string.
pub fn prepareAlloc(self: *Self, allocator: std.mem.Allocator, sql: []const u8) !Statement {
const sql_z = try allocator.dupeZ(u8, sql);
defer allocator.free(sql_z);
return self.prepare(sql_z);
}
// ========================================================================
// Database Info
// ========================================================================
/// Returns the rowid of the most recent successful INSERT.
pub fn lastInsertRowId(self: *Self) i64 {
return c.sqlite3_last_insert_rowid(self.handle);
}
/// Returns the number of rows modified by the most recent statement.
pub fn changes(self: *Self) i32 {
return c.sqlite3_changes(self.handle);
}
/// Returns the total number of rows modified since connection opened.
pub fn totalChanges(self: *Self) i32 {
return c.sqlite3_total_changes(self.handle);
}
/// Returns the most recent error message.
pub fn errorMessage(self: *Self) ?[]const u8 {
const msg = c.sqlite3_errmsg(self.handle);
if (msg) |m| {
return std.mem.span(m);
}
return null;
}
/// Returns the error code of the most recent error.
pub fn errorCode(self: *Self) i32 {
return c.sqlite3_errcode(self.handle);
}
/// Returns the extended error code of the most recent error.
pub fn extendedErrorCode(self: *Self) i32 {
return c.sqlite3_extended_errcode(self.handle);
}
/// Returns whether the database is read-only.
pub fn isReadOnly(self: *Self, db_name: [:0]const u8) bool {
return c.sqlite3_db_readonly(self.handle, db_name.ptr) == 1;
}
/// Returns the filename of a database.
pub fn filename(self: *Self, db_name: [:0]const u8) ?[]const u8 {
const fname = c.sqlite3_db_filename(self.handle, db_name.ptr);
if (fname) |f| {
return std.mem.span(f);
}
return null;
}
/// Interrupts a long-running query.
pub fn interrupt(self: *Self) void {
c.sqlite3_interrupt(self.handle);
}
// ========================================================================
// Transactions
// ========================================================================
/// Enables or disables foreign key constraints.
pub fn setForeignKeys(self: *Self, enabled: bool) Error!void {
const sql: [:0]const u8 = if (enabled) "PRAGMA foreign_keys = ON" else "PRAGMA foreign_keys = OFF";
try self.exec(sql);
}
/// Begins a transaction.
pub fn begin(self: *Self) Error!void {
try self.exec("BEGIN");
}
/// Begins an immediate transaction.
pub fn beginImmediate(self: *Self) Error!void {
try self.exec("BEGIN IMMEDIATE");
}
/// Begins an exclusive transaction.
pub fn beginExclusive(self: *Self) Error!void {
try self.exec("BEGIN EXCLUSIVE");
}
/// Commits the current transaction.
pub fn commit(self: *Self) Error!void {
try self.exec("COMMIT");
}
/// Rolls back the current transaction.
pub fn rollback(self: *Self) Error!void {
try self.exec("ROLLBACK");
}
// ========================================================================
// Savepoints
// ========================================================================
/// Creates a savepoint with the given name.
pub fn savepoint(self: *Self, allocator: std.mem.Allocator, name: []const u8) !void {
const sql = try std.fmt.allocPrint(allocator, "SAVEPOINT {s}\x00", .{name});
defer allocator.free(sql);
try self.exec(sql[0 .. sql.len - 1 :0]);
}
/// Releases (commits) a savepoint.
pub fn release(self: *Self, allocator: std.mem.Allocator, name: []const u8) !void {
const sql = try std.fmt.allocPrint(allocator, "RELEASE SAVEPOINT {s}\x00", .{name});
defer allocator.free(sql);
try self.exec(sql[0 .. sql.len - 1 :0]);
}
/// Rolls back to a savepoint.
pub fn rollbackTo(self: *Self, allocator: std.mem.Allocator, name: []const u8) !void {
const sql = try std.fmt.allocPrint(allocator, "ROLLBACK TO SAVEPOINT {s}\x00", .{name});
defer allocator.free(sql);
try self.exec(sql[0 .. sql.len - 1 :0]);
}
// ========================================================================
// Pragmas and Configuration
// ========================================================================
/// Sets the busy timeout in milliseconds.
pub fn setBusyTimeout(self: *Self, ms: i32) Error!void {
const result = c.sqlite3_busy_timeout(self.handle, ms);
if (result != c.SQLITE_OK) {
return resultToError(result);
}
}
/// Sets the journal mode.
pub fn setJournalMode(self: *Self, allocator: std.mem.Allocator, mode: []const u8) !void {
const sql = try std.fmt.allocPrint(allocator, "PRAGMA journal_mode = {s}\x00", .{mode});
defer allocator.free(sql);
try self.exec(sql[0 .. sql.len - 1 :0]);
}
/// Sets the synchronous mode.
pub fn setSynchronous(self: *Self, allocator: std.mem.Allocator, mode: []const u8) !void {
const sql = try std.fmt.allocPrint(allocator, "PRAGMA synchronous = {s}\x00", .{mode});
defer allocator.free(sql);
try self.exec(sql[0 .. sql.len - 1 :0]);
}
/// Enables WAL mode with recommended settings.
pub fn enableWalMode(self: *Self, allocator: std.mem.Allocator) !void {
try self.setJournalMode(allocator, "WAL");
try self.setSynchronous(allocator, "NORMAL");
}
/// Sets the auto_vacuum mode.
pub fn setAutoVacuum(self: *Self, allocator: std.mem.Allocator, mode: []const u8) !void {
const sql = try std.fmt.allocPrint(allocator, "PRAGMA auto_vacuum = {s}\x00", .{mode});
defer allocator.free(sql);
try self.exec(sql[0 .. sql.len - 1 :0]);
}
/// Sets the cache size.
pub fn setCacheSize(self: *Self, allocator: std.mem.Allocator, size: i32) !void {
const sql = try std.fmt.allocPrint(allocator, "PRAGMA cache_size = {d}\x00", .{size});
defer allocator.free(sql);
try self.exec(sql[0 .. sql.len - 1 :0]);
}
/// Enables or disables case-sensitive LIKE.
pub fn setCaseSensitiveLike(self: *Self, enabled: bool) Error!void {
const sql: [:0]const u8 = if (enabled) "PRAGMA case_sensitive_like = ON" else "PRAGMA case_sensitive_like = OFF";
try self.exec(sql);
}
/// Enables or disables deferred foreign key enforcement.
pub fn setDeferForeignKeys(self: *Self, enabled: bool) Error!void {
const sql: [:0]const u8 = if (enabled) "PRAGMA defer_foreign_keys = ON" else "PRAGMA defer_foreign_keys = OFF";
try self.exec(sql);
}
/// Sets the locking mode.
pub fn setLockingMode(self: *Self, allocator: std.mem.Allocator, mode: []const u8) !void {
const sql = try std.fmt.allocPrint(allocator, "PRAGMA locking_mode = {s}\x00", .{mode});
defer allocator.free(sql);
try self.exec(sql[0 .. sql.len - 1 :0]);
}
/// Enables or disables query_only mode.
pub fn setQueryOnly(self: *Self, enabled: bool) Error!void {
const sql: [:0]const u8 = if (enabled) "PRAGMA query_only = ON" else "PRAGMA query_only = OFF";
try self.exec(sql);
}
/// Enables or disables recursive triggers.
pub fn setRecursiveTriggers(self: *Self, enabled: bool) Error!void {
const sql: [:0]const u8 = if (enabled) "PRAGMA recursive_triggers = ON" else "PRAGMA recursive_triggers = OFF";
try self.exec(sql);
}
/// Enables or disables secure delete.
pub fn setSecureDelete(self: *Self, enabled: bool) Error!void {
const sql: [:0]const u8 = if (enabled) "PRAGMA secure_delete = ON" else "PRAGMA secure_delete = OFF";
try self.exec(sql);
}
/// Sets the page size.
pub fn setPageSize(self: *Self, allocator: std.mem.Allocator, size: u32) !void {
const sql = try std.fmt.allocPrint(allocator, "PRAGMA page_size = {d}\x00", .{size});
defer allocator.free(sql);
try self.exec(sql[0 .. sql.len - 1 :0]);
}
/// Sets the maximum page count.
pub fn setMaxPageCount(self: *Self, allocator: std.mem.Allocator, count: u32) !void {
const sql = try std.fmt.allocPrint(allocator, "PRAGMA max_page_count = {d}\x00", .{count});
defer allocator.free(sql);
try self.exec(sql[0 .. sql.len - 1 :0]);
}
/// Sets the temp_store location.
pub fn setTempStore(self: *Self, allocator: std.mem.Allocator, mode: []const u8) !void {
const sql = try std.fmt.allocPrint(allocator, "PRAGMA temp_store = {s}\x00", .{mode});
defer allocator.free(sql);
try self.exec(sql[0 .. sql.len - 1 :0]);
}
/// Sets the WAL auto-checkpoint interval.
pub fn setWalAutoCheckpoint(self: *Self, allocator: std.mem.Allocator, pages: u32) !void {
const sql = try std.fmt.allocPrint(allocator, "PRAGMA wal_autocheckpoint = {d}\x00", .{pages});
defer allocator.free(sql);
try self.exec(sql[0 .. sql.len - 1 :0]);
}
// ========================================================================
// Maintenance
// ========================================================================
/// Runs integrity check and returns result.
pub fn integrityCheck(self: *Self, allocator: std.mem.Allocator) ![]u8 {
var stmt = try self.prepare("PRAGMA integrity_check");
defer stmt.finalize();
if (try stmt.step()) {
if (stmt.columnText(0)) |text| {
return try allocator.dupe(u8, text);
}
}
return try allocator.dupe(u8, "unknown");
}
/// Runs quick integrity check.
pub fn quickCheck(self: *Self, allocator: std.mem.Allocator) ![]u8 {
var stmt = try self.prepare("PRAGMA quick_check");
defer stmt.finalize();
if (try stmt.step()) {
if (stmt.columnText(0)) |text| {
return try allocator.dupe(u8, text);
}
}
return try allocator.dupe(u8, "unknown");
}
/// Runs VACUUM to rebuild the database file.
pub fn vacuum(self: *Self) Error!void {
try self.exec("VACUUM");
}
/// Runs incremental vacuum.
pub fn incrementalVacuum(self: *Self, allocator: std.mem.Allocator, pages: ?u32) !void {
if (pages) |p| {
const sql = try std.fmt.allocPrint(allocator, "PRAGMA incremental_vacuum({d})\x00", .{p});
defer allocator.free(sql);
try self.exec(sql[0 .. sql.len - 1 :0]);
} else {
try self.exec("PRAGMA incremental_vacuum");
}
}
/// Performs a WAL checkpoint.
pub fn walCheckpoint(self: *Self, allocator: std.mem.Allocator, mode: []const u8) !void {
const sql = try std.fmt.allocPrint(allocator, "PRAGMA wal_checkpoint({s})\x00", .{mode});
defer allocator.free(sql);
try self.exec(sql[0 .. sql.len - 1 :0]);
}
/// Optimizes the database.
pub fn optimize(self: *Self) Error!void {
try self.exec("PRAGMA optimize");
}
// ========================================================================
// ATTACH/DETACH
// ========================================================================
/// Attaches another database file to this connection.
pub fn attach(self: *Self, allocator: std.mem.Allocator, file_path: []const u8, schema_name: []const u8) !void {
const sql = try std.fmt.allocPrint(allocator, "ATTACH DATABASE '{s}' AS {s}\x00", .{ file_path, schema_name });
defer allocator.free(sql);
try self.exec(sql[0 .. sql.len - 1 :0]);
}
/// Attaches an in-memory database to this connection.
pub fn attachMemory(self: *Self, allocator: std.mem.Allocator, schema_name: []const u8) !void {
const sql = try std.fmt.allocPrint(allocator, "ATTACH DATABASE ':memory:' AS {s}\x00", .{schema_name});
defer allocator.free(sql);
try self.exec(sql[0 .. sql.len - 1 :0]);
}
/// Detaches a previously attached database.
pub fn detach(self: *Self, allocator: std.mem.Allocator, schema_name: []const u8) !void {
const sql = try std.fmt.allocPrint(allocator, "DETACH DATABASE {s}\x00", .{schema_name});
defer allocator.free(sql);
try self.exec(sql[0 .. sql.len - 1 :0]);
}
/// Returns a list of attached database names.
pub fn listDatabases(self: *Self, allocator: std.mem.Allocator) ![][]const u8 {
var stmt = try self.prepare("PRAGMA database_list");
defer stmt.finalize();
var list: std.ArrayListUnmanaged([]const u8) = .empty;
errdefer {
for (list.items) |item| allocator.free(item);
list.deinit(allocator);
}
while (try stmt.step()) {
if (stmt.columnText(1)) |name| {
const owned = try allocator.dupe(u8, name);
try list.append(allocator, owned);
}
}
return list.toOwnedSlice(allocator);
}
/// Frees the list returned by listDatabases.
pub fn freeDatabaseList(allocator: std.mem.Allocator, list: [][]const u8) void {
for (list) |item| allocator.free(item);
allocator.free(list);
}
// ========================================================================
// User-Defined Functions
// ========================================================================
/// Registers a scalar function with the database.
pub fn createScalarFunction(self: *Self, name: [:0]const u8, num_args: i32, func: ScalarFn) !void {
const wrapper = try ScalarFnWrapper.create(func);
const result = c.sqlite3_create_function_v2(
self.handle,
name.ptr,
num_args,
c.SQLITE_UTF8,
wrapper,
functions.scalarCallback,
null,
null,
functions.scalarDestructor,
);
if (result != c.SQLITE_OK) {
wrapper.destroy();
return resultToError(result);
}
}
/// Removes a function from the database.
pub fn removeFunction(self: *Self, name: [:0]const u8, num_args: i32) Error!void {
const result = c.sqlite3_create_function_v2(
self.handle,
name.ptr,
num_args,
c.SQLITE_UTF8,
null,
null,
null,
null,
null,
);
if (result != c.SQLITE_OK) {
return resultToError(result);
}
}
/// Registers an aggregate function with the database.
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,
functions.aggregateStepCallback,
functions.aggregateFinalCallback,
functions.aggregateDestructor,
);
if (result != c.SQLITE_OK) {
wrapper.destroy();
return resultToError(result);
}
}
/// Registers a window function with the database.
pub fn createWindowFunction(
self: *Self,
name: [:0]const u8,
num_args: i32,
step_fn: AggregateStepFn,
final_fn: AggregateFinalFn,
value_fn: WindowValueFn,
inverse_fn: WindowInverseFn,
) !void {
const wrapper = try WindowFnWrapper.create(step_fn, final_fn, value_fn, inverse_fn);
const result = c.sqlite3_create_window_function(
self.handle,
name.ptr,
num_args,
c.SQLITE_UTF8,
wrapper,
functions.windowStepCallback,
functions.windowFinalCallback,
functions.windowValueCallback,
functions.windowInverseCallback,
functions.windowDestructor,
);
if (result != c.SQLITE_OK) {
wrapper.destroy();
return resultToError(result);
}
}
// ========================================================================
// Collations
// ========================================================================
/// Registers a custom collation sequence.
pub fn createCollation(self: *Self, name: [:0]const u8, func: CollationFn) !void {
const wrapper = try CollationWrapper.create(func);
const result = c.sqlite3_create_collation_v2(
self.handle,
name.ptr,
c.SQLITE_UTF8,
wrapper,
functions.collationCallback,
functions.collationDestructor,
);
if (result != c.SQLITE_OK) {
wrapper.destroy();
return resultToError(result);
}
}
/// Removes a custom collation sequence.
pub fn removeCollation(self: *Self, name: [:0]const u8) Error!void {
const result = c.sqlite3_create_collation_v2(
self.handle,
name.ptr,
c.SQLITE_UTF8,
null,
null,
null,
);
if (result != c.SQLITE_OK) {
return resultToError(result);
}
}
// ========================================================================
// Hooks
// ========================================================================
/// Sets a commit hook callback.
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, functions.commitHookCallback, wrapper);
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.
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, functions.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.
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, functions.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();
}
}
}
/// Sets a pre-update hook callback.
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, functions.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.
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();
}
}
// ========================================================================
// Authorizer
// ========================================================================
/// Sets an authorizer callback.
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, functions.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.
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, functions.progressCallback, wrapper);
} else {
c.sqlite3_progress_handler(self.handle, 0, null, null);
}
}
// ========================================================================
// Busy Handler
// ========================================================================
/// Sets a custom busy handler callback.
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, functions.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);
}
// ========================================================================
// File Control
// ========================================================================
/// File control operation codes.
pub const FileControlOp = enum(c_int) {
lockstate = 1,
get_lockproxyfile = 2,
set_lockproxyfile = 3,
last_errno = 4,
size_hint = 5,
chunk_size = 6,
file_pointer = 7,
sync_omitted = 8,
win32_av_retry = 9,
persist_wal = 10,
overwrite = 11,
vfsname = 12,
powersafe_overwrite = 13,
pragma = 14,
busyhandler = 15,
tempfilename = 16,
mmap_size = 18,
trace = 19,
has_moved = 20,
sync = 21,
commit_phasetwo = 22,
win32_set_handle = 23,
wal_block = 24,
zipvfs = 25,
rbu = 26,
vfs_pointer = 27,
journal_pointer = 28,
win32_get_handle = 29,
pdb = 30,
begin_atomic_write = 31,
commit_atomic_write = 32,
rollback_atomic_write = 33,
lock_timeout = 34,
data_version = 35,
size_limit = 36,
ckpt_done = 37,
reserve_bytes = 38,
ckpt_start = 39,
external_reader = 40,
cksm_file = 41,
reset_cache = 42,
};
/// Performs a file control operation with an integer value.
///
/// This is a low-level interface to the sqlite3_file_control() function.
/// Most applications should use the higher-level pragma interface instead.
///
/// Parameters:
/// - db_name: The database name ("main", "temp", or attached db name)
/// - op: The file control operation code
/// - value: Pointer to the integer value (input/output depending on operation)
///
/// Returns error on failure.
pub fn setFileControlInt(self: *Self, db_name: [:0]const u8, op: FileControlOp, value: *i32) Error!void {
const result = c.sqlite3_file_control(self.handle, db_name.ptr, @intFromEnum(op), value);
if (result != c.SQLITE_OK) {
return resultToError(result);
}
}
/// Gets persist_wal setting.
pub fn getPersistWal(self: *Self, db_name: [:0]const u8) Error!bool {
var value: i32 = -1;
try self.setFileControlInt(db_name, .persist_wal, &value);
return value != 0;
}
/// Sets persist_wal setting.
pub fn setPersistWal(self: *Self, db_name: [:0]const u8, persist: bool) Error!void {
var value: i32 = if (persist) 1 else 0;
try self.setFileControlInt(db_name, .persist_wal, &value);
}
/// Gets powersafe_overwrite setting.
pub fn getPowersafeOverwrite(self: *Self, db_name: [:0]const u8) Error!bool {
var value: i32 = -1;
try self.setFileControlInt(db_name, .powersafe_overwrite, &value);
return value != 0;
}
/// Sets powersafe_overwrite setting.
pub fn setPowersafeOverwrite(self: *Self, db_name: [:0]const u8, enabled: bool) Error!void {
var value: i32 = if (enabled) 1 else 0;
try self.setFileControlInt(db_name, .powersafe_overwrite, &value);
}
/// Sets the chunk size for file growth.
pub fn setChunkSize(self: *Self, db_name: [:0]const u8, size: i32) Error!void {
var value: i32 = size;
try self.setFileControlInt(db_name, .chunk_size, &value);
}
/// Gets the data version (incremented each time the database is modified).
pub fn getDataVersion(self: *Self, db_name: [:0]const u8) Error!u32 {
var value: i32 = 0;
try self.setFileControlInt(db_name, .data_version, &value);
return @intCast(value);
}
};

142
src/errors.zig Normal file
View file

@ -0,0 +1,142 @@
//! SQLite error handling
//!
//! Maps SQLite error codes to Zig errors for idiomatic error handling.
const c = @import("c.zig").c;
/// SQLite error codes mapped to Zig errors
pub const Error = error{
/// Generic error
SqliteError,
/// Internal logic error in SQLite
InternalError,
/// Access permission denied
PermissionDenied,
/// Callback routine requested an abort
Abort,
/// The database file is locked
Busy,
/// A table in the database is locked
Locked,
/// A malloc() failed
OutOfMemory,
/// Attempt to write a readonly database
ReadOnly,
/// Operation terminated by sqlite3_interrupt()
Interrupt,
/// Some kind of disk I/O error occurred
IoError,
/// The database disk image is malformed
Corrupt,
/// Unknown opcode in sqlite3_file_control()
NotFound,
/// Insertion failed because database is full
Full,
/// Unable to open the database file
CantOpen,
/// Database lock protocol error
Protocol,
/// Internal use only
Empty,
/// The database schema changed
Schema,
/// String or BLOB exceeds size limit
TooBig,
/// Abort due to constraint violation
Constraint,
/// Data type mismatch
Mismatch,
/// Library used incorrectly
Misuse,
/// Uses OS features not supported on host
NoLfs,
/// Authorization denied
Auth,
/// Not used
Format,
/// Parameter out of range
Range,
/// File opened that is not a database file
NotADatabase,
/// Notifications from sqlite3_log()
Notice,
/// Warnings from sqlite3_log()
Warning,
/// sqlite3_step() has another row ready
Row,
/// sqlite3_step() has finished executing
Done,
};
/// Converts a SQLite result code to a Zig error
pub fn resultToError(result: c_int) Error {
return switch (result) {
c.SQLITE_ERROR => Error.SqliteError,
c.SQLITE_INTERNAL => Error.InternalError,
c.SQLITE_PERM => Error.PermissionDenied,
c.SQLITE_ABORT => Error.Abort,
c.SQLITE_BUSY => Error.Busy,
c.SQLITE_LOCKED => Error.Locked,
c.SQLITE_NOMEM => Error.OutOfMemory,
c.SQLITE_READONLY => Error.ReadOnly,
c.SQLITE_INTERRUPT => Error.Interrupt,
c.SQLITE_IOERR => Error.IoError,
c.SQLITE_CORRUPT => Error.Corrupt,
c.SQLITE_NOTFOUND => Error.NotFound,
c.SQLITE_FULL => Error.Full,
c.SQLITE_CANTOPEN => Error.CantOpen,
c.SQLITE_PROTOCOL => Error.Protocol,
c.SQLITE_EMPTY => Error.Empty,
c.SQLITE_SCHEMA => Error.Schema,
c.SQLITE_TOOBIG => Error.TooBig,
c.SQLITE_CONSTRAINT => Error.Constraint,
c.SQLITE_MISMATCH => Error.Mismatch,
c.SQLITE_MISUSE => Error.Misuse,
c.SQLITE_NOLFS => Error.NoLfs,
c.SQLITE_AUTH => Error.Auth,
c.SQLITE_FORMAT => Error.Format,
c.SQLITE_RANGE => Error.Range,
c.SQLITE_NOTADB => Error.NotADatabase,
c.SQLITE_NOTICE => Error.Notice,
c.SQLITE_WARNING => Error.Warning,
c.SQLITE_ROW => Error.Row,
c.SQLITE_DONE => Error.Done,
else => Error.SqliteError,
};
}
/// Returns a human-readable description for an error
pub fn errorDescription(err: Error) []const u8 {
return switch (err) {
Error.SqliteError => "Generic SQLite error",
Error.InternalError => "Internal logic error in SQLite",
Error.PermissionDenied => "Access permission denied",
Error.Abort => "Callback routine requested an abort",
Error.Busy => "The database file is locked",
Error.Locked => "A table in the database is locked",
Error.OutOfMemory => "A malloc() failed",
Error.ReadOnly => "Attempt to write a readonly database",
Error.Interrupt => "Operation terminated by sqlite3_interrupt()",
Error.IoError => "Some kind of disk I/O error occurred",
Error.Corrupt => "The database disk image is malformed",
Error.NotFound => "Unknown opcode in sqlite3_file_control()",
Error.Full => "Insertion failed because database is full",
Error.CantOpen => "Unable to open the database file",
Error.Protocol => "Database lock protocol error",
Error.Empty => "Internal use only",
Error.Schema => "The database schema changed",
Error.TooBig => "String or BLOB exceeds size limit",
Error.Constraint => "Abort due to constraint violation",
Error.Mismatch => "Data type mismatch",
Error.Misuse => "Library used incorrectly",
Error.NoLfs => "Uses OS features not supported on host",
Error.Auth => "Authorization denied",
Error.Format => "Not used",
Error.Range => "Parameter out of range",
Error.NotADatabase => "File opened that is not a database file",
Error.Notice => "Notifications from sqlite3_log()",
Error.Warning => "Warnings from sqlite3_log()",
Error.Row => "sqlite3_step() has another row ready",
Error.Done => "sqlite3_step() has finished executing",
};
}

229
src/fts5.zig Normal file
View file

@ -0,0 +1,229 @@
//! FTS5 Full-Text Search Helpers
//!
//! Provides convenient wrappers for SQLite's FTS5 full-text search extension.
//! FTS5 is compiled into SQLite by default with -DSQLITE_ENABLE_FTS5.
const std = @import("std");
const Database = @import("database.zig").Database;
const Statement = @import("statement.zig").Statement;
const Error = @import("errors.zig").Error;
/// FTS5 tokenizer options.
pub const TokenizerOption = enum {
unicode61,
ascii,
porter,
trigram,
pub fn name(self: TokenizerOption) []const u8 {
return switch (self) {
.unicode61 => "unicode61",
.ascii => "ascii",
.porter => "porter",
.trigram => "trigram",
};
}
};
/// FTS5 helper functions for a database connection.
pub const Fts5 = struct {
db: *Database,
allocator: std.mem.Allocator,
const Self = @This();
/// Creates a new FTS5 helper bound to a database connection.
pub fn init(db: *Database, allocator: std.mem.Allocator) Self {
return .{ .db = db, .allocator = allocator };
}
/// Creates a simple FTS5 table with specified columns.
pub fn createSimpleTable(self: *Self, table_name: []const u8, columns: []const []const u8) !void {
try self.createTableWithTokenizer(table_name, columns, .unicode61);
}
/// Creates an FTS5 table with a specific tokenizer.
pub fn createTableWithTokenizer(
self: *Self,
table_name: []const u8,
columns: []const []const u8,
tokenizer: TokenizerOption,
) !void {
// Build column list
var col_buf: [4096]u8 = undefined;
var col_len: usize = 0;
for (columns, 0..) |col, i| {
if (i > 0) {
col_buf[col_len] = ',';
col_buf[col_len + 1] = ' ';
col_len += 2;
}
@memcpy(col_buf[col_len..][0..col.len], col);
col_len += col.len;
}
const sql = try std.fmt.allocPrint(
self.allocator,
"CREATE VIRTUAL TABLE {s} USING fts5({s}, tokenize='{s}')\x00",
.{ table_name, col_buf[0..col_len], tokenizer.name() },
);
defer self.allocator.free(sql);
try self.db.exec(sql[0 .. sql.len - 1 :0]);
}
/// Drops an FTS5 table.
pub fn dropTable(self: *Self, table_name: []const u8) !void {
const sql = try std.fmt.allocPrint(self.allocator, "DROP TABLE IF EXISTS {s}\x00", .{table_name});
defer self.allocator.free(sql);
try self.db.exec(sql[0 .. sql.len - 1 :0]);
}
/// Performs a full-text search query.
pub fn search(
self: *Self,
table_name: []const u8,
query: []const u8,
result_columns: []const []const u8,
limit: ?u32,
) !Statement {
// Build column list
var col_buf: [4096]u8 = undefined;
var col_len: usize = 0;
if (result_columns.len == 0) {
col_buf[0] = '*';
col_len = 1;
} else {
for (result_columns, 0..) |col, i| {
if (i > 0) {
col_buf[col_len] = ',';
col_buf[col_len + 1] = ' ';
col_len += 2;
}
@memcpy(col_buf[col_len..][0..col.len], col);
col_len += col.len;
}
}
const sql = if (limit) |l|
try std.fmt.allocPrint(
self.allocator,
"SELECT {s} FROM {s} WHERE {s} MATCH ? ORDER BY rank LIMIT {d}\x00",
.{ col_buf[0..col_len], table_name, table_name, l },
)
else
try std.fmt.allocPrint(
self.allocator,
"SELECT {s} FROM {s} WHERE {s} MATCH ? ORDER BY rank\x00",
.{ col_buf[0..col_len], table_name, table_name },
);
defer self.allocator.free(sql);
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
try stmt.bindText(1, query);
return stmt;
}
/// Gets highlighted snippet from search results.
pub fn searchWithHighlight(
self: *Self,
table_name: []const u8,
query: []const u8,
column_index: u32,
before_match: []const u8,
after_match: []const u8,
limit: ?u32,
) !Statement {
const sql = if (limit) |l|
try std.fmt.allocPrint(
self.allocator,
"SELECT highlight({s}, {d}, '{s}', '{s}') FROM {s} WHERE {s} MATCH ? ORDER BY rank LIMIT {d}\x00",
.{ table_name, column_index, before_match, after_match, table_name, table_name, l },
)
else
try std.fmt.allocPrint(
self.allocator,
"SELECT highlight({s}, {d}, '{s}', '{s}') FROM {s} WHERE {s} MATCH ? ORDER BY rank\x00",
.{ table_name, column_index, before_match, after_match, table_name, table_name },
);
defer self.allocator.free(sql);
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
try stmt.bindText(1, query);
return stmt;
}
/// Gets a snippet (context around matches) from search results.
pub fn searchWithSnippet(
self: *Self,
table_name: []const u8,
query: []const u8,
column_index: i32,
before_match: []const u8,
after_match: []const u8,
ellipsis: []const u8,
max_tokens: u32,
limit: ?u32,
) !Statement {
const sql = if (limit) |l|
try std.fmt.allocPrint(
self.allocator,
"SELECT snippet({s}, {d}, '{s}', '{s}', '{s}', {d}) FROM {s} WHERE {s} MATCH ? ORDER BY rank LIMIT {d}\x00",
.{ table_name, column_index, before_match, after_match, ellipsis, max_tokens, table_name, table_name, l },
)
else
try std.fmt.allocPrint(
self.allocator,
"SELECT snippet({s}, {d}, '{s}', '{s}', '{s}', {d}) FROM {s} WHERE {s} MATCH ? ORDER BY rank\x00",
.{ table_name, column_index, before_match, after_match, ellipsis, max_tokens, table_name, table_name },
);
defer self.allocator.free(sql);
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
try stmt.bindText(1, query);
return stmt;
}
/// Rebuilds the FTS5 index.
pub fn rebuild(self: *Self, table_name: []const u8) !void {
const sql = try std.fmt.allocPrint(self.allocator, "INSERT INTO {s}({s}) VALUES('rebuild')\x00", .{ table_name, table_name });
defer self.allocator.free(sql);
try self.db.exec(sql[0 .. sql.len - 1 :0]);
}
/// Optimizes the FTS5 index by merging segments.
pub fn optimize(self: *Self, table_name: []const u8) !void {
const sql = try std.fmt.allocPrint(self.allocator, "INSERT INTO {s}({s}) VALUES('optimize')\x00", .{ table_name, table_name });
defer self.allocator.free(sql);
try self.db.exec(sql[0 .. sql.len - 1 :0]);
}
/// Runs integrity check on the FTS5 index.
pub fn integrityCheck(self: *Self, table_name: []const u8) !void {
const sql = try std.fmt.allocPrint(self.allocator, "INSERT INTO {s}({s}) VALUES('integrity-check')\x00", .{ table_name, table_name });
defer self.allocator.free(sql);
try self.db.exec(sql[0 .. sql.len - 1 :0]);
}
/// Deletes all content from the FTS5 table.
pub fn deleteAll(self: *Self, table_name: []const u8) !void {
const sql = try std.fmt.allocPrint(self.allocator, "DELETE FROM {s}\x00", .{table_name});
defer self.allocator.free(sql);
try self.db.exec(sql[0 .. sql.len - 1 :0]);
}
/// Gets the total number of rows in the FTS5 table.
pub fn count(self: *Self, table_name: []const u8) !i64 {
const sql = try std.fmt.allocPrint(self.allocator, "SELECT COUNT(*) FROM {s}\x00", .{table_name});
defer self.allocator.free(sql);
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
defer stmt.finalize();
if (try stmt.step()) {
return stmt.columnInt(0);
}
return 0;
}
};

567
src/functions.zig Normal file
View file

@ -0,0 +1,567 @@
//! User-defined functions and hooks for SQLite
//!
//! Provides support for scalar functions, aggregate functions, window functions,
//! collations, and database hooks (commit, rollback, update, pre-update).
const std = @import("std");
const c = @import("c.zig").c;
const types = @import("types.zig");
const ColumnType = types.ColumnType;
const UpdateOperation = types.UpdateOperation;
const AuthAction = types.AuthAction;
const AuthResult = types.AuthResult;
// ============================================================================
// Function Context and Values
// ============================================================================
/// Context for user-defined function results.
pub const FunctionContext = struct {
ctx: *c.sqlite3_context,
const Self = @This();
pub fn setNull(self: Self) void {
c.sqlite3_result_null(self.ctx);
}
pub fn setInt(self: Self, value: i64) void {
c.sqlite3_result_int64(self.ctx, value);
}
pub fn setFloat(self: Self, value: f64) void {
c.sqlite3_result_double(self.ctx, value);
}
pub fn setText(self: Self, value: []const u8) void {
c.sqlite3_result_text(self.ctx, value.ptr, @intCast(value.len), c.SQLITE_TRANSIENT);
}
pub fn setBlob(self: Self, value: []const u8) void {
c.sqlite3_result_blob(self.ctx, value.ptr, @intCast(value.len), c.SQLITE_TRANSIENT);
}
pub fn setError(self: Self, msg: []const u8) void {
c.sqlite3_result_error(self.ctx, msg.ptr, @intCast(msg.len));
}
};
/// Value passed to user-defined functions.
pub const FunctionValue = struct {
value: *c.sqlite3_value,
const Self = @This();
pub fn getType(self: Self) ColumnType {
const vtype = c.sqlite3_value_type(self.value);
return switch (vtype) {
c.SQLITE_INTEGER => .integer,
c.SQLITE_FLOAT => .float,
c.SQLITE_TEXT => .text,
c.SQLITE_BLOB => .blob,
c.SQLITE_NULL => .null_value,
else => .null_value,
};
}
pub fn isNull(self: Self) bool {
return self.getType() == .null_value;
}
pub fn asInt(self: Self) i64 {
return c.sqlite3_value_int64(self.value);
}
pub fn asFloat(self: Self) f64 {
return c.sqlite3_value_double(self.value);
}
pub fn asText(self: Self) ?[]const u8 {
const len = c.sqlite3_value_bytes(self.value);
const text = c.sqlite3_value_text(self.value);
if (text) |t| {
return t[0..@intCast(len)];
}
return null;
}
pub fn asBlob(self: Self) ?[]const u8 {
const len = c.sqlite3_value_bytes(self.value);
const blob = c.sqlite3_value_blob(self.value);
if (blob) |b| {
const ptr: [*]const u8 = @ptrCast(b);
return ptr[0..@intCast(len)];
}
return null;
}
};
/// Context for aggregate functions with state management.
pub const AggregateContext = struct {
ctx: *c.sqlite3_context,
const Self = @This();
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));
}
pub fn setNull(self: Self) void {
c.sqlite3_result_null(self.ctx);
}
pub fn setInt(self: Self, value: i64) void {
c.sqlite3_result_int64(self.ctx, value);
}
pub fn setFloat(self: Self, value: f64) void {
c.sqlite3_result_double(self.ctx, value);
}
pub fn setText(self: Self, value: []const u8) void {
c.sqlite3_result_text(self.ctx, value.ptr, @intCast(value.len), c.SQLITE_TRANSIENT);
}
pub fn setBlob(self: Self, value: []const u8) void {
c.sqlite3_result_blob(self.ctx, value.ptr, @intCast(value.len), c.SQLITE_TRANSIENT);
}
pub fn setError(self: Self, msg: []const u8) void {
c.sqlite3_result_error(self.ctx, msg.ptr, @intCast(msg.len));
}
};
/// Context for pre-update hook with access to old/new values.
pub const PreUpdateContext = struct {
db: *c.sqlite3,
const Self = @This();
pub fn columnCount(self: Self) i32 {
return c.sqlite3_preupdate_count(self.db);
}
pub fn depth(self: Self) i32 {
return c.sqlite3_preupdate_depth(self.db);
}
pub fn oldValue(self: Self, 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.? };
}
pub fn newValue(self: Self, 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.? };
}
};
// ============================================================================
// Function Types
// ============================================================================
pub const ScalarFn = *const fn (ctx: FunctionContext, args: []const FunctionValue) void;
pub const AggregateStepFn = *const fn (ctx: AggregateContext, args: []const FunctionValue) void;
pub const AggregateFinalFn = *const fn (ctx: AggregateContext) void;
pub const WindowValueFn = *const fn (ctx: AggregateContext) void;
pub const WindowInverseFn = *const fn (ctx: AggregateContext, args: []const FunctionValue) void;
pub const CollationFn = *const fn (a: []const u8, b: []const u8) i32;
// Hook function types
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 ZigPreUpdateHookFn = *const fn (ctx: PreUpdateContext, operation: UpdateOperation, db_name: []const u8, table_name: []const u8, old_rowid: i64, new_rowid: i64) void;
pub const ZigAuthorizerFn = *const fn (action: AuthAction, arg1: ?[]const u8, arg2: ?[]const u8, arg3: ?[]const u8, arg4: ?[]const u8) AuthResult;
pub const ZigProgressFn = *const fn () bool;
pub const ZigBusyHandlerFn = *const fn (count: i32) bool;
// ============================================================================
// Wrappers (stored in SQLite user_data)
// ============================================================================
pub const ScalarFnWrapper = struct {
func: ScalarFn,
pub fn create(func: ScalarFn) !*ScalarFnWrapper {
const wrapper = try std.heap.page_allocator.create(ScalarFnWrapper);
wrapper.func = func;
return wrapper;
}
pub fn destroy(self: *ScalarFnWrapper) void {
std.heap.page_allocator.destroy(self);
}
};
pub const AggregateFnWrapper = struct {
step_fn: AggregateStepFn,
final_fn: AggregateFinalFn,
pub 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;
}
pub fn destroy(self: *AggregateFnWrapper) void {
std.heap.page_allocator.destroy(self);
}
};
pub const WindowFnWrapper = struct {
step_fn: AggregateStepFn,
final_fn: AggregateFinalFn,
value_fn: WindowValueFn,
inverse_fn: WindowInverseFn,
pub fn create(step_fn: AggregateStepFn, final_fn: AggregateFinalFn, value_fn: WindowValueFn, inverse_fn: WindowInverseFn) !*WindowFnWrapper {
const wrapper = try std.heap.page_allocator.create(WindowFnWrapper);
wrapper.step_fn = step_fn;
wrapper.final_fn = final_fn;
wrapper.value_fn = value_fn;
wrapper.inverse_fn = inverse_fn;
return wrapper;
}
pub fn destroy(self: *WindowFnWrapper) void {
std.heap.page_allocator.destroy(self);
}
};
pub const CollationWrapper = struct {
func: CollationFn,
pub fn create(func: CollationFn) !*CollationWrapper {
const wrapper = try std.heap.page_allocator.create(CollationWrapper);
wrapper.func = func;
return wrapper;
}
pub fn destroy(self: *CollationWrapper) void {
std.heap.page_allocator.destroy(self);
}
};
pub const CommitHookWrapper = struct {
func: ZigCommitHookFn,
pub fn create(func: ZigCommitHookFn) !*CommitHookWrapper {
const wrapper = try std.heap.page_allocator.create(CommitHookWrapper);
wrapper.func = func;
return wrapper;
}
pub fn destroy(self: *CommitHookWrapper) void {
std.heap.page_allocator.destroy(self);
}
};
pub const RollbackHookWrapper = struct {
func: ZigRollbackHookFn,
pub fn create(func: ZigRollbackHookFn) !*RollbackHookWrapper {
const wrapper = try std.heap.page_allocator.create(RollbackHookWrapper);
wrapper.func = func;
return wrapper;
}
pub fn destroy(self: *RollbackHookWrapper) void {
std.heap.page_allocator.destroy(self);
}
};
pub const UpdateHookWrapper = struct {
func: ZigUpdateHookFn,
pub fn create(func: ZigUpdateHookFn) !*UpdateHookWrapper {
const wrapper = try std.heap.page_allocator.create(UpdateHookWrapper);
wrapper.func = func;
return wrapper;
}
pub fn destroy(self: *UpdateHookWrapper) void {
std.heap.page_allocator.destroy(self);
}
};
pub const PreUpdateHookWrapper = struct {
func: ZigPreUpdateHookFn,
pub fn create(func: ZigPreUpdateHookFn) !*PreUpdateHookWrapper {
const wrapper = try std.heap.page_allocator.create(PreUpdateHookWrapper);
wrapper.func = func;
return wrapper;
}
pub fn destroy(self: *PreUpdateHookWrapper) void {
std.heap.page_allocator.destroy(self);
}
};
pub const AuthorizerWrapper = struct {
func: ZigAuthorizerFn,
pub fn create(func: ZigAuthorizerFn) !*AuthorizerWrapper {
const wrapper = try std.heap.page_allocator.create(AuthorizerWrapper);
wrapper.func = func;
return wrapper;
}
pub fn destroy(self: *AuthorizerWrapper) void {
std.heap.page_allocator.destroy(self);
}
};
pub const ProgressWrapper = struct {
func: ZigProgressFn,
pub fn create(func: ZigProgressFn) !*ProgressWrapper {
const wrapper = try std.heap.page_allocator.create(ProgressWrapper);
wrapper.func = func;
return wrapper;
}
pub fn destroy(self: *ProgressWrapper) void {
std.heap.page_allocator.destroy(self);
}
};
pub const BusyHandlerWrapper = struct {
func: ZigBusyHandlerFn,
pub fn create(func: ZigBusyHandlerFn) !*BusyHandlerWrapper {
const wrapper = try std.heap.page_allocator.create(BusyHandlerWrapper);
wrapper.func = func;
return wrapper;
}
pub fn destroy(self: *BusyHandlerWrapper) void {
std.heap.page_allocator.destroy(self);
}
};
// ============================================================================
// C Callback Trampolines
// ============================================================================
pub fn scalarCallback(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: *ScalarFnWrapper = @ptrCast(@alignCast(user_data));
const func_ctx = FunctionContext{ .ctx = ctx.? };
const args_count: usize = @intCast(argc);
var args: [16]FunctionValue = undefined;
const actual_count = @min(args_count, 16);
for (0..actual_count) |i| {
if (argv[i]) |v| {
args[i] = FunctionValue{ .value = v };
}
}
wrapper.func(func_ctx, args[0..actual_count]);
}
pub fn scalarDestructor(ptr: ?*anyopaque) callconv(.c) void {
if (ptr) |p| {
const wrapper: *ScalarFnWrapper = @ptrCast(@alignCast(p));
wrapper.destroy();
}
}
pub 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.? };
const args_count: usize = @intCast(argc);
var args: [16]FunctionValue = undefined;
const actual_count = @min(args_count, 16);
for (0..actual_count) |i| {
if (argv[i]) |v| {
args[i] = FunctionValue{ .value = v };
}
}
wrapper.step_fn(agg_ctx, args[0..actual_count]);
}
pub 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);
}
pub fn aggregateDestructor(ptr: ?*anyopaque) callconv(.c) void {
if (ptr) |p| {
const wrapper: *AggregateFnWrapper = @ptrCast(@alignCast(p));
wrapper.destroy();
}
}
pub fn windowStepCallback(ctx: ?*c.sqlite3_context, argc: c_int, argv: [*c]?*c.sqlite3_value) callconv(.c) void {
const user_data = c.sqlite3_user_data(ctx);
if (user_data == null) return;
const wrapper: *WindowFnWrapper = @ptrCast(@alignCast(user_data));
const agg_ctx = AggregateContext{ .ctx = ctx.? };
const args_count: usize = @intCast(argc);
var args: [16]FunctionValue = undefined;
const actual_count = @min(args_count, 16);
for (0..actual_count) |i| {
if (argv[i]) |v| {
args[i] = FunctionValue{ .value = v };
}
}
wrapper.step_fn(agg_ctx, args[0..actual_count]);
}
pub fn windowFinalCallback(ctx: ?*c.sqlite3_context) callconv(.c) void {
const user_data = c.sqlite3_user_data(ctx);
if (user_data == null) return;
const wrapper: *WindowFnWrapper = @ptrCast(@alignCast(user_data));
const agg_ctx = AggregateContext{ .ctx = ctx.? };
wrapper.final_fn(agg_ctx);
}
pub fn windowValueCallback(ctx: ?*c.sqlite3_context) callconv(.c) void {
const user_data = c.sqlite3_user_data(ctx);
if (user_data == null) return;
const wrapper: *WindowFnWrapper = @ptrCast(@alignCast(user_data));
const agg_ctx = AggregateContext{ .ctx = ctx.? };
wrapper.value_fn(agg_ctx);
}
pub fn windowInverseCallback(ctx: ?*c.sqlite3_context, argc: c_int, argv: [*c]?*c.sqlite3_value) callconv(.c) void {
const user_data = c.sqlite3_user_data(ctx);
if (user_data == null) return;
const wrapper: *WindowFnWrapper = @ptrCast(@alignCast(user_data));
const agg_ctx = AggregateContext{ .ctx = ctx.? };
const args_count: usize = @intCast(argc);
var args: [16]FunctionValue = undefined;
const actual_count = @min(args_count, 16);
for (0..actual_count) |i| {
if (argv[i]) |v| {
args[i] = FunctionValue{ .value = v };
}
}
wrapper.inverse_fn(agg_ctx, args[0..actual_count]);
}
pub fn windowDestructor(ptr: ?*anyopaque) callconv(.c) void {
if (ptr) |p| {
const wrapper: *WindowFnWrapper = @ptrCast(@alignCast(p));
wrapper.destroy();
}
}
pub fn collationCallback(user_data: ?*anyopaque, len_a: c_int, data_a: ?*const anyopaque, len_b: c_int, data_b: ?*const anyopaque) callconv(.c) c_int {
if (user_data == null) return 0;
const wrapper: *CollationWrapper = @ptrCast(@alignCast(user_data));
const a: []const u8 = if (data_a) |ptr|
@as([*]const u8, @ptrCast(ptr))[0..@intCast(len_a)]
else
"";
const b: []const u8 = if (data_b) |ptr|
@as([*]const u8, @ptrCast(ptr))[0..@intCast(len_b)]
else
"";
return wrapper.func(a, b);
}
pub fn collationDestructor(ptr: ?*anyopaque) callconv(.c) void {
if (ptr) |p| {
const wrapper: *CollationWrapper = @ptrCast(@alignCast(p));
wrapper.destroy();
}
}
pub 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;
}
pub fn rollbackHookCallback(user_data: ?*anyopaque) callconv(.c) void {
if (user_data == null) return;
const wrapper: *RollbackHookWrapper = @ptrCast(@alignCast(user_data));
wrapper.func();
}
pub 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);
}
pub 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);
}
pub 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 a1: ?[]const u8 = if (arg1 != null) std.mem.span(arg1) else null;
const a2: ?[]const u8 = if (arg2 != null) std.mem.span(arg2) else null;
const a3: ?[]const u8 = if (arg3 != null) std.mem.span(arg3) else null;
const a4: ?[]const u8 = if (arg4 != null) std.mem.span(arg4) else null;
const result = wrapper.func(auth_action, a1, a2, a3, a4);
return @intFromEnum(result);
}
pub 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;
}
pub 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;
}

437
src/json.zig Normal file
View file

@ -0,0 +1,437 @@
//! JSON1 Extension Helpers
//!
//! Provides convenient wrappers for SQLite's JSON1 extension functions.
//! JSON1 is compiled into SQLite by default with -DSQLITE_ENABLE_JSON1.
const std = @import("std");
const Database = @import("database.zig").Database;
const Statement = @import("statement.zig").Statement;
const Error = @import("errors.zig").Error;
/// JSON helper functions for a database connection.
pub const Json = struct {
db: *Database,
allocator: std.mem.Allocator,
const Self = @This();
/// Creates a new JSON helper bound to a database connection.
pub fn init(db: *Database, allocator: std.mem.Allocator) Self {
return .{ .db = db, .allocator = allocator };
}
// ========================================================================
// JSON Validation and Parsing
// ========================================================================
/// Validates and minifies a JSON string.
pub fn validate(self: *Self, json_text: []const u8) !?[]u8 {
var stmt = try self.db.prepare("SELECT json(?)");
defer stmt.finalize();
try stmt.bindText(1, json_text);
if (try stmt.step()) {
if (stmt.columnText(0)) |text| {
return try self.allocator.dupe(u8, text);
}
}
return null;
}
/// Checks if a string is valid JSON.
pub fn isValid(self: *Self, json_text: []const u8) !bool {
var stmt = try self.db.prepare("SELECT json_valid(?)");
defer stmt.finalize();
try stmt.bindText(1, json_text);
if (try stmt.step()) {
return stmt.columnInt(0) == 1;
}
return false;
}
/// Returns the JSON type of a value at a path.
pub fn getType(self: *Self, json_text: []const u8, path: []const u8) !?[]u8 {
var stmt = try self.db.prepare("SELECT json_type(?, ?)");
defer stmt.finalize();
try stmt.bindText(1, json_text);
try stmt.bindText(2, path);
if (try stmt.step()) {
if (stmt.columnText(0)) |text| {
return try self.allocator.dupe(u8, text);
}
}
return null;
}
// ========================================================================
// JSON Extraction
// ========================================================================
/// Extracts a value from JSON at the given path.
pub fn extract(self: *Self, json_text: []const u8, path: []const u8) !?[]u8 {
var stmt = try self.db.prepare("SELECT json_extract(?, ?)");
defer stmt.finalize();
try stmt.bindText(1, json_text);
try stmt.bindText(2, path);
if (try stmt.step()) {
if (stmt.columnText(0)) |text| {
return try self.allocator.dupe(u8, text);
}
}
return null;
}
/// Extracts an integer from JSON at the given path.
pub fn extractInt(self: *Self, json_text: []const u8, path: []const u8) !?i64 {
var stmt = try self.db.prepare("SELECT json_extract(?, ?)");
defer stmt.finalize();
try stmt.bindText(1, json_text);
try stmt.bindText(2, path);
if (try stmt.step()) {
if (!stmt.columnIsNull(0)) {
return stmt.columnInt(0);
}
}
return null;
}
/// Extracts a float from JSON at the given path.
pub fn extractFloat(self: *Self, json_text: []const u8, path: []const u8) !?f64 {
var stmt = try self.db.prepare("SELECT json_extract(?, ?)");
defer stmt.finalize();
try stmt.bindText(1, json_text);
try stmt.bindText(2, path);
if (try stmt.step()) {
if (!stmt.columnIsNull(0)) {
return stmt.columnFloat(0);
}
}
return null;
}
/// Extracts a boolean from JSON at the given path.
pub fn extractBool(self: *Self, json_text: []const u8, path: []const u8) !?bool {
const result = try self.extractInt(json_text, path);
if (result) |v| {
return v != 0;
}
return null;
}
// ========================================================================
// JSON Modification
// ========================================================================
/// Inserts a value into JSON at the given path (only if path doesn't exist).
pub fn insert(self: *Self, json_text: []const u8, path: []const u8, value: []const u8) ![]u8 {
var stmt = try self.db.prepare("SELECT json_insert(?, ?, json(?))");
defer stmt.finalize();
try stmt.bindText(1, json_text);
try stmt.bindText(2, path);
try stmt.bindText(3, value);
if (try stmt.step()) {
if (stmt.columnText(0)) |text| {
return try self.allocator.dupe(u8, text);
}
}
return try self.allocator.dupe(u8, json_text);
}
/// Replaces a value in JSON at the given path (only if path exists).
pub fn replace(self: *Self, json_text: []const u8, path: []const u8, value: []const u8) ![]u8 {
var stmt = try self.db.prepare("SELECT json_replace(?, ?, json(?))");
defer stmt.finalize();
try stmt.bindText(1, json_text);
try stmt.bindText(2, path);
try stmt.bindText(3, value);
if (try stmt.step()) {
if (stmt.columnText(0)) |text| {
return try self.allocator.dupe(u8, text);
}
}
return try self.allocator.dupe(u8, json_text);
}
/// Sets a value in JSON at the given path (insert or replace).
pub fn set(self: *Self, json_text: []const u8, path: []const u8, value: []const u8) ![]u8 {
var stmt = try self.db.prepare("SELECT json_set(?, ?, json(?))");
defer stmt.finalize();
try stmt.bindText(1, json_text);
try stmt.bindText(2, path);
try stmt.bindText(3, value);
if (try stmt.step()) {
if (stmt.columnText(0)) |text| {
return try self.allocator.dupe(u8, text);
}
}
return try self.allocator.dupe(u8, json_text);
}
/// Sets a string value in JSON at the given path.
pub fn setString(self: *Self, json_text: []const u8, path: []const u8, value: []const u8) ![]u8 {
var stmt = try self.db.prepare("SELECT json_set(?, ?, ?)");
defer stmt.finalize();
try stmt.bindText(1, json_text);
try stmt.bindText(2, path);
try stmt.bindText(3, value);
if (try stmt.step()) {
if (stmt.columnText(0)) |text| {
return try self.allocator.dupe(u8, text);
}
}
return try self.allocator.dupe(u8, json_text);
}
/// Sets an integer value in JSON at the given path.
pub fn setInt(self: *Self, json_text: []const u8, path: []const u8, value: i64) ![]u8 {
var stmt = try self.db.prepare("SELECT json_set(?, ?, ?)");
defer stmt.finalize();
try stmt.bindText(1, json_text);
try stmt.bindText(2, path);
try stmt.bindInt(3, value);
if (try stmt.step()) {
if (stmt.columnText(0)) |text| {
return try self.allocator.dupe(u8, text);
}
}
return try self.allocator.dupe(u8, json_text);
}
/// Sets a float value in JSON at the given path.
pub fn setFloat(self: *Self, json_text: []const u8, path: []const u8, value: f64) ![]u8 {
var stmt = try self.db.prepare("SELECT json_set(?, ?, ?)");
defer stmt.finalize();
try stmt.bindText(1, json_text);
try stmt.bindText(2, path);
try stmt.bindFloat(3, value);
if (try stmt.step()) {
if (stmt.columnText(0)) |text| {
return try self.allocator.dupe(u8, text);
}
}
return try self.allocator.dupe(u8, json_text);
}
/// Sets a boolean value in JSON at the given path.
pub fn setBool(self: *Self, json_text: []const u8, path: []const u8, value: bool) ![]u8 {
const json_bool = if (value) "true" else "false";
var stmt = try self.db.prepare("SELECT json_set(?, ?, json(?))");
defer stmt.finalize();
try stmt.bindText(1, json_text);
try stmt.bindText(2, path);
try stmt.bindText(3, json_bool);
if (try stmt.step()) {
if (stmt.columnText(0)) |text| {
return try self.allocator.dupe(u8, text);
}
}
return try self.allocator.dupe(u8, json_text);
}
/// Removes a value from JSON at the given path.
pub fn remove(self: *Self, json_text: []const u8, path: []const u8) ![]u8 {
var stmt = try self.db.prepare("SELECT json_remove(?, ?)");
defer stmt.finalize();
try stmt.bindText(1, json_text);
try stmt.bindText(2, path);
if (try stmt.step()) {
if (stmt.columnText(0)) |text| {
return try self.allocator.dupe(u8, text);
}
}
return try self.allocator.dupe(u8, json_text);
}
// ========================================================================
// JSON Array Operations
// ========================================================================
/// Returns the length of a JSON array.
pub fn arrayLength(self: *Self, json_text: []const u8, path: ?[]const u8) !?i64 {
if (path) |p| {
var stmt = try self.db.prepare("SELECT json_array_length(?, ?)");
defer stmt.finalize();
try stmt.bindText(1, json_text);
try stmt.bindText(2, p);
if (try stmt.step()) {
if (!stmt.columnIsNull(0)) {
return stmt.columnInt(0);
}
}
} else {
var stmt = try self.db.prepare("SELECT json_array_length(?)");
defer stmt.finalize();
try stmt.bindText(1, json_text);
if (try stmt.step()) {
if (!stmt.columnIsNull(0)) {
return stmt.columnInt(0);
}
}
}
return null;
}
/// Creates a JSON array from string values.
pub fn createArray(self: *Self, values: []const []const u8) ![]u8 {
if (values.len == 0) {
return try self.allocator.dupe(u8, "[]");
}
// Build placeholders
var placeholders: [256]u8 = undefined;
var ph_len: usize = 0;
for (0..values.len) |i| {
if (i > 0) {
placeholders[ph_len] = ',';
placeholders[ph_len + 1] = ' ';
ph_len += 2;
}
placeholders[ph_len] = '?';
ph_len += 1;
}
const sql = try std.fmt.allocPrint(self.allocator, "SELECT json_array({s})\x00", .{placeholders[0..ph_len]});
defer self.allocator.free(sql);
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
defer stmt.finalize();
for (values, 0..) |v, i| {
try stmt.bindText(@intCast(i + 1), v);
}
if (try stmt.step()) {
if (stmt.columnText(0)) |text| {
return try self.allocator.dupe(u8, text);
}
}
return try self.allocator.dupe(u8, "[]");
}
/// Creates a JSON object from key-value pairs.
pub fn createObject(self: *Self, keys: []const []const u8, values: []const []const u8) ![]u8 {
if (keys.len != values.len) {
return error.OutOfMemory; // Use a generic error
}
if (keys.len == 0) {
return try self.allocator.dupe(u8, "{}");
}
// Build placeholders
var placeholders: [512]u8 = undefined;
var ph_len: usize = 0;
for (0..keys.len) |i| {
if (i > 0) {
placeholders[ph_len] = ',';
placeholders[ph_len + 1] = ' ';
ph_len += 2;
}
placeholders[ph_len] = '?';
placeholders[ph_len + 1] = ',';
placeholders[ph_len + 2] = ' ';
placeholders[ph_len + 3] = '?';
ph_len += 4;
}
const sql = try std.fmt.allocPrint(self.allocator, "SELECT json_object({s})\x00", .{placeholders[0..ph_len]});
defer self.allocator.free(sql);
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
defer stmt.finalize();
var param_idx: u32 = 1;
for (keys, values) |k, v| {
try stmt.bindText(param_idx, k);
param_idx += 1;
try stmt.bindText(param_idx, v);
param_idx += 1;
}
if (try stmt.step()) {
if (stmt.columnText(0)) |text| {
return try self.allocator.dupe(u8, text);
}
}
return try self.allocator.dupe(u8, "{}");
}
/// Applies a JSON patch (RFC 7396) to a JSON document.
pub fn patch(self: *Self, target: []const u8, patch_doc: []const u8) ![]u8 {
var stmt = try self.db.prepare("SELECT json_patch(?, ?)");
defer stmt.finalize();
try stmt.bindText(1, target);
try stmt.bindText(2, patch_doc);
if (try stmt.step()) {
if (stmt.columnText(0)) |text| {
return try self.allocator.dupe(u8, text);
}
}
return try self.allocator.dupe(u8, target);
}
/// Returns a statement for iterating over JSON elements with json_each.
pub fn each(self: *Self, json_text: []const u8, path: ?[]const u8) !Statement {
if (path) |p| {
var stmt = try self.db.prepare(
"SELECT key, value, type, atom, id, parent, fullkey, path FROM json_each(?, ?)",
);
try stmt.bindText(1, json_text);
try stmt.bindText(2, p);
return stmt;
} else {
var stmt = try self.db.prepare(
"SELECT key, value, type, atom, id, parent, fullkey, path FROM json_each(?)",
);
try stmt.bindText(1, json_text);
return stmt;
}
}
/// Returns a statement for iterating over all JSON elements with json_tree.
pub fn tree(self: *Self, json_text: []const u8, path: ?[]const u8) !Statement {
if (path) |p| {
var stmt = try self.db.prepare(
"SELECT key, value, type, atom, id, parent, fullkey, path FROM json_tree(?, ?)",
);
try stmt.bindText(1, json_text);
try stmt.bindText(2, p);
return stmt;
} else {
var stmt = try self.db.prepare(
"SELECT key, value, type, atom, id, parent, fullkey, path FROM json_tree(?)",
);
try stmt.bindText(1, json_text);
return stmt;
}
}
};

151
src/pool.zig Normal file
View file

@ -0,0 +1,151 @@
//! SQLite Connection Pool
//!
//! Provides a simple thread-safe connection pool for managing
//! multiple database connections.
const std = @import("std");
const Database = @import("database.zig").Database;
/// A simple connection pool for SQLite databases.
///
/// Manages a pool of database connections that can be acquired and released
/// for concurrent access. All connections share the same database file.
///
/// Example:
/// ```zig
/// var pool = try ConnectionPool.init(allocator, "mydb.sqlite", 4);
/// defer pool.deinit();
///
/// var conn = try pool.acquire();
/// defer pool.release(conn);
///
/// try conn.exec("SELECT ...");
/// ```
pub const ConnectionPool = struct {
allocator: std.mem.Allocator,
path: []u8,
connections: []?Database,
in_use: []bool,
mutex: std.Thread.Mutex,
max_size: usize,
const Self = @This();
/// Creates a new connection pool.
///
/// Parameters:
/// - `allocator`: Allocator for pool management
/// - `path`: Database file path
/// - `max_size`: Maximum number of connections in the pool
pub fn init(allocator: std.mem.Allocator, path: [:0]const u8, max_size: usize) !Self {
const path_copy = try allocator.dupe(u8, path);
errdefer allocator.free(path_copy);
const connections = try allocator.alloc(?Database, max_size);
errdefer allocator.free(connections);
@memset(connections, null);
const in_use = try allocator.alloc(bool, max_size);
errdefer allocator.free(in_use);
@memset(in_use, false);
return .{
.allocator = allocator,
.path = path_copy,
.connections = connections,
.in_use = in_use,
.mutex = .{},
.max_size = max_size,
};
}
/// Destroys the connection pool, closing all connections.
pub fn deinit(self: *Self) void {
for (self.connections) |*conn_opt| {
if (conn_opt.*) |*conn| {
conn.close();
}
}
self.allocator.free(self.connections);
self.allocator.free(self.in_use);
self.allocator.free(self.path);
}
/// Acquires a connection from the pool.
///
/// Returns an existing idle connection or creates a new one if the pool
/// isn't full. Returns error if all connections are in use and pool is full.
pub fn acquire(self: *Self) !*Database {
self.mutex.lock();
defer self.mutex.unlock();
// Look for an existing idle connection
for (self.connections, 0..) |*conn_opt, i| {
if (conn_opt.* != null and !self.in_use[i]) {
self.in_use[i] = true;
return &conn_opt.*.?;
}
}
// Look for an empty slot to create a new connection
for (self.connections, 0..) |*conn_opt, i| {
if (conn_opt.* == null) {
// Create null-terminated path
const path_z = self.allocator.dupeZ(u8, self.path) catch return error.OutOfMemory;
defer self.allocator.free(path_z);
conn_opt.* = Database.open(path_z) catch |e| {
return e;
};
self.in_use[i] = true;
return &conn_opt.*.?;
}
}
return error.Busy; // Pool exhausted
}
/// Releases a connection back to the pool.
pub fn release(self: *Self, conn: *Database) void {
self.mutex.lock();
defer self.mutex.unlock();
for (self.connections, 0..) |*conn_opt, i| {
if (conn_opt.*) |*stored_conn| {
if (stored_conn == conn) {
self.in_use[i] = false;
return;
}
}
}
}
/// Returns the number of connections currently in use.
pub fn inUseCount(self: *Self) usize {
self.mutex.lock();
defer self.mutex.unlock();
var count: usize = 0;
for (self.in_use) |used| {
if (used) count += 1;
}
return count;
}
/// Returns the total number of open connections (idle + in use).
pub fn openCount(self: *Self) usize {
self.mutex.lock();
defer self.mutex.unlock();
var count: usize = 0;
for (self.connections) |conn_opt| {
if (conn_opt != null) count += 1;
}
return count;
}
/// Returns the maximum pool size.
pub fn capacity(self: *Self) usize {
return self.max_size;
}
};

File diff suppressed because it is too large Load diff

564
src/rtree.zig Normal file
View file

@ -0,0 +1,564 @@
//! R-Tree Spatial Index Helpers
//!
//! Provides convenient wrappers for SQLite's R-Tree extension for spatial indexing.
//! R-Tree is compiled into SQLite by default with -DSQLITE_ENABLE_RTREE.
//!
//! R-Tree is useful for:
//! - Geographic data (latitude/longitude bounding boxes)
//! - 2D/3D spatial queries
//! - Range queries on multiple dimensions
//! - Game collision detection
//! - Any multi-dimensional range search
const std = @import("std");
const Database = @import("database.zig").Database;
const Statement = @import("statement.zig").Statement;
const Error = @import("errors.zig").Error;
/// A 2D bounding box (rectangle).
pub const BoundingBox2D = struct {
min_x: f64,
max_x: f64,
min_y: f64,
max_y: f64,
/// Creates a bounding box from center point and size.
pub fn fromCenter(center_x: f64, center_y: f64, width: f64, height: f64) BoundingBox2D {
const half_w = width / 2.0;
const half_h = height / 2.0;
return .{
.min_x = center_x - half_w,
.max_x = center_x + half_w,
.min_y = center_y - half_h,
.max_y = center_y + half_h,
};
}
/// Creates a bounding box from a point (zero-size box).
pub fn fromPoint(x: f64, y: f64) BoundingBox2D {
return .{
.min_x = x,
.max_x = x,
.min_y = y,
.max_y = y,
};
}
/// Checks if this box intersects with another box.
pub fn intersects(self: BoundingBox2D, other: BoundingBox2D) bool {
return self.min_x <= other.max_x and
self.max_x >= other.min_x and
self.min_y <= other.max_y and
self.max_y >= other.min_y;
}
/// Checks if this box contains a point.
pub fn containsPoint(self: BoundingBox2D, x: f64, y: f64) bool {
return x >= self.min_x and x <= self.max_x and
y >= self.min_y and y <= self.max_y;
}
/// Checks if this box fully contains another box.
pub fn contains(self: BoundingBox2D, other: BoundingBox2D) bool {
return other.min_x >= self.min_x and
other.max_x <= self.max_x and
other.min_y >= self.min_y and
other.max_y <= self.max_y;
}
/// Returns the area of the bounding box.
pub fn area(self: BoundingBox2D) f64 {
return (self.max_x - self.min_x) * (self.max_y - self.min_y);
}
/// Returns the center point of the bounding box.
pub fn center(self: BoundingBox2D) struct { x: f64, y: f64 } {
return .{
.x = (self.min_x + self.max_x) / 2.0,
.y = (self.min_y + self.max_y) / 2.0,
};
}
/// Expands this box to include another box.
pub fn expand(self: *BoundingBox2D, other: BoundingBox2D) void {
self.min_x = @min(self.min_x, other.min_x);
self.max_x = @max(self.max_x, other.max_x);
self.min_y = @min(self.min_y, other.min_y);
self.max_y = @max(self.max_y, other.max_y);
}
};
/// A 3D bounding box.
pub const BoundingBox3D = struct {
min_x: f64,
max_x: f64,
min_y: f64,
max_y: f64,
min_z: f64,
max_z: f64,
/// Creates a 3D bounding box from center point and size.
pub fn fromCenter(center_x: f64, center_y: f64, center_z: f64, width: f64, height: f64, depth: f64) BoundingBox3D {
const half_w = width / 2.0;
const half_h = height / 2.0;
const half_d = depth / 2.0;
return .{
.min_x = center_x - half_w,
.max_x = center_x + half_w,
.min_y = center_y - half_h,
.max_y = center_y + half_h,
.min_z = center_z - half_d,
.max_z = center_z + half_d,
};
}
/// Creates a bounding box from a point (zero-size box).
pub fn fromPoint(x: f64, y: f64, z: f64) BoundingBox3D {
return .{
.min_x = x,
.max_x = x,
.min_y = y,
.max_y = y,
.min_z = z,
.max_z = z,
};
}
/// Checks if this box intersects with another box.
pub fn intersects(self: BoundingBox3D, other: BoundingBox3D) bool {
return self.min_x <= other.max_x and
self.max_x >= other.min_x and
self.min_y <= other.max_y and
self.max_y >= other.min_y and
self.min_z <= other.max_z and
self.max_z >= other.min_z;
}
/// Returns the volume of the bounding box.
pub fn volume(self: BoundingBox3D) f64 {
return (self.max_x - self.min_x) * (self.max_y - self.min_y) * (self.max_z - self.min_z);
}
};
/// Geographic coordinates (latitude/longitude).
pub const GeoCoord = struct {
latitude: f64,
longitude: f64,
/// Creates a bounding box around this point with a given radius in degrees.
pub fn boundingBox(self: GeoCoord, radius_degrees: f64) BoundingBox2D {
return .{
.min_x = self.longitude - radius_degrees,
.max_x = self.longitude + radius_degrees,
.min_y = self.latitude - radius_degrees,
.max_y = self.latitude + radius_degrees,
};
}
/// Approximate distance in kilometers to another coordinate (Haversine formula).
pub fn distanceKm(self: GeoCoord, other: GeoCoord) f64 {
const earth_radius_km = 6371.0;
const lat1 = self.latitude * std.math.pi / 180.0;
const lat2 = other.latitude * std.math.pi / 180.0;
const delta_lat = (other.latitude - self.latitude) * std.math.pi / 180.0;
const delta_lon = (other.longitude - self.longitude) * std.math.pi / 180.0;
const a = std.math.sin(delta_lat / 2.0) * std.math.sin(delta_lat / 2.0) +
std.math.cos(lat1) * std.math.cos(lat2) *
std.math.sin(delta_lon / 2.0) * std.math.sin(delta_lon / 2.0);
const c = 2.0 * std.math.atan2(std.math.sqrt(a), std.math.sqrt(1.0 - a));
return earth_radius_km * c;
}
};
/// R-Tree helper functions for a database connection.
pub const RTree = struct {
db: *Database,
allocator: std.mem.Allocator,
const Self = @This();
/// Creates a new R-Tree helper bound to a database connection.
pub fn init(db: *Database, allocator: std.mem.Allocator) Self {
return .{ .db = db, .allocator = allocator };
}
// ========================================================================
// Table Management
// ========================================================================
/// Creates a 2D R-Tree virtual table.
///
/// Example:
/// ```zig
/// var rtree = RTree.init(&db, allocator);
/// try rtree.createTable2D("locations", "min_x", "max_x", "min_y", "max_y");
/// ```
pub fn createTable2D(
self: *Self,
table_name: []const u8,
min_x_name: []const u8,
max_x_name: []const u8,
min_y_name: []const u8,
max_y_name: []const u8,
) !void {
const sql = try std.fmt.allocPrint(
self.allocator,
"CREATE VIRTUAL TABLE {s} USING rtree(id, {s}, {s}, {s}, {s})\x00",
.{ table_name, min_x_name, max_x_name, min_y_name, max_y_name },
);
defer self.allocator.free(sql);
try self.db.exec(sql[0 .. sql.len - 1 :0]);
}
/// Creates a 3D R-Tree virtual table.
pub fn createTable3D(
self: *Self,
table_name: []const u8,
min_x_name: []const u8,
max_x_name: []const u8,
min_y_name: []const u8,
max_y_name: []const u8,
min_z_name: []const u8,
max_z_name: []const u8,
) !void {
const sql = try std.fmt.allocPrint(
self.allocator,
"CREATE VIRTUAL TABLE {s} USING rtree(id, {s}, {s}, {s}, {s}, {s}, {s})\x00",
.{ table_name, min_x_name, max_x_name, min_y_name, max_y_name, min_z_name, max_z_name },
);
defer self.allocator.free(sql);
try self.db.exec(sql[0 .. sql.len - 1 :0]);
}
/// Creates a simple 2D R-Tree with default column names.
pub fn createSimpleTable2D(self: *Self, table_name: []const u8) !void {
try self.createTable2D(table_name, "min_x", "max_x", "min_y", "max_y");
}
/// Creates a simple 3D R-Tree with default column names.
pub fn createSimpleTable3D(self: *Self, table_name: []const u8) !void {
try self.createTable3D(table_name, "min_x", "max_x", "min_y", "max_y", "min_z", "max_z");
}
/// Creates a geographic R-Tree (using latitude/longitude).
pub fn createGeoTable(self: *Self, table_name: []const u8) !void {
try self.createTable2D(table_name, "min_lon", "max_lon", "min_lat", "max_lat");
}
/// Drops an R-Tree table.
pub fn dropTable(self: *Self, table_name: []const u8) !void {
const sql = try std.fmt.allocPrint(self.allocator, "DROP TABLE IF EXISTS {s}\x00", .{table_name});
defer self.allocator.free(sql);
try self.db.exec(sql[0 .. sql.len - 1 :0]);
}
// ========================================================================
// Insert Operations
// ========================================================================
/// Inserts a 2D bounding box into an R-Tree.
pub fn insert2D(self: *Self, table_name: []const u8, id: i64, box: BoundingBox2D) !void {
const sql = try std.fmt.allocPrint(
self.allocator,
"INSERT INTO {s} VALUES (?, ?, ?, ?, ?)\x00",
.{table_name},
);
defer self.allocator.free(sql);
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
defer stmt.finalize();
try stmt.bindInt(1, id);
try stmt.bindFloat(2, box.min_x);
try stmt.bindFloat(3, box.max_x);
try stmt.bindFloat(4, box.min_y);
try stmt.bindFloat(5, box.max_y);
_ = try stmt.step();
}
/// Inserts a 3D bounding box into an R-Tree.
pub fn insert3D(self: *Self, table_name: []const u8, id: i64, box: BoundingBox3D) !void {
const sql = try std.fmt.allocPrint(
self.allocator,
"INSERT INTO {s} VALUES (?, ?, ?, ?, ?, ?, ?)\x00",
.{table_name},
);
defer self.allocator.free(sql);
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
defer stmt.finalize();
try stmt.bindInt(1, id);
try stmt.bindFloat(2, box.min_x);
try stmt.bindFloat(3, box.max_x);
try stmt.bindFloat(4, box.min_y);
try stmt.bindFloat(5, box.max_y);
try stmt.bindFloat(6, box.min_z);
try stmt.bindFloat(7, box.max_z);
_ = try stmt.step();
}
/// Inserts a point (as a zero-size bounding box) into a 2D R-Tree.
pub fn insertPoint2D(self: *Self, table_name: []const u8, id: i64, x: f64, y: f64) !void {
try self.insert2D(table_name, id, BoundingBox2D.fromPoint(x, y));
}
/// Inserts a geographic coordinate into a geo R-Tree.
pub fn insertGeo(self: *Self, table_name: []const u8, id: i64, coord: GeoCoord) !void {
try self.insert2D(table_name, id, .{
.min_x = coord.longitude,
.max_x = coord.longitude,
.min_y = coord.latitude,
.max_y = coord.latitude,
});
}
// ========================================================================
// Update Operations
// ========================================================================
/// Updates a 2D bounding box in an R-Tree.
pub fn update2D(self: *Self, table_name: []const u8, id: i64, box: BoundingBox2D) !void {
const sql = try std.fmt.allocPrint(
self.allocator,
"UPDATE {s} SET min_x=?, max_x=?, min_y=?, max_y=? WHERE id=?\x00",
.{table_name},
);
defer self.allocator.free(sql);
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
defer stmt.finalize();
try stmt.bindFloat(1, box.min_x);
try stmt.bindFloat(2, box.max_x);
try stmt.bindFloat(3, box.min_y);
try stmt.bindFloat(4, box.max_y);
try stmt.bindInt(5, id);
_ = try stmt.step();
}
/// Deletes an entry from an R-Tree.
pub fn delete(self: *Self, table_name: []const u8, id: i64) !void {
const sql = try std.fmt.allocPrint(
self.allocator,
"DELETE FROM {s} WHERE id=?\x00",
.{table_name},
);
defer self.allocator.free(sql);
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
defer stmt.finalize();
try stmt.bindInt(1, id);
_ = try stmt.step();
}
// ========================================================================
// Query Operations
// ========================================================================
/// Queries for all entries that intersect with a bounding box.
/// Returns a prepared statement with results: id, min_x, max_x, min_y, max_y
pub fn queryIntersects2D(self: *Self, table_name: []const u8, box: BoundingBox2D) !Statement {
const sql = try std.fmt.allocPrint(
self.allocator,
"SELECT * FROM {s} WHERE min_x <= ? AND max_x >= ? AND min_y <= ? AND max_y >= ?\x00",
.{table_name},
);
defer self.allocator.free(sql);
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
errdefer stmt.finalize();
try stmt.bindFloat(1, box.max_x);
try stmt.bindFloat(2, box.min_x);
try stmt.bindFloat(3, box.max_y);
try stmt.bindFloat(4, box.min_y);
return stmt;
}
/// Queries for all entries contained within a bounding box.
pub fn queryContained2D(self: *Self, table_name: []const u8, box: BoundingBox2D) !Statement {
const sql = try std.fmt.allocPrint(
self.allocator,
"SELECT * FROM {s} WHERE min_x >= ? AND max_x <= ? AND min_y >= ? AND max_y <= ?\x00",
.{table_name},
);
defer self.allocator.free(sql);
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
errdefer stmt.finalize();
try stmt.bindFloat(1, box.min_x);
try stmt.bindFloat(2, box.max_x);
try stmt.bindFloat(3, box.min_y);
try stmt.bindFloat(4, box.max_y);
return stmt;
}
/// Queries for all entries near a point (within a given radius).
pub fn queryNearPoint2D(self: *Self, table_name: []const u8, x: f64, y: f64, radius: f64) !Statement {
const box = BoundingBox2D{
.min_x = x - radius,
.max_x = x + radius,
.min_y = y - radius,
.max_y = y + radius,
};
return self.queryIntersects2D(table_name, box);
}
/// Queries for all entries near a geographic coordinate.
/// radius is in degrees (approximately: 1 degree 111km at equator)
pub fn queryNearGeo(self: *Self, table_name: []const u8, coord: GeoCoord, radius_degrees: f64) !Statement {
return self.queryNearPoint2D(table_name, coord.longitude, coord.latitude, radius_degrees);
}
/// Gets all entry IDs that intersect with a bounding box.
pub fn getIntersectingIds2D(self: *Self, table_name: []const u8, box: BoundingBox2D) ![]i64 {
var stmt = try self.queryIntersects2D(table_name, box);
defer stmt.finalize();
var ids: std.ArrayListUnmanaged(i64) = .empty;
errdefer ids.deinit(self.allocator);
while (try stmt.step()) {
try ids.append(self.allocator, stmt.columnInt(0));
}
return ids.toOwnedSlice(self.allocator);
}
/// Frees an array of IDs returned by getIntersectingIds2D.
pub fn freeIds(self: *Self, ids: []i64) void {
self.allocator.free(ids);
}
/// Counts entries that intersect with a bounding box.
pub fn countIntersects2D(self: *Self, table_name: []const u8, box: BoundingBox2D) !i64 {
const sql = try std.fmt.allocPrint(
self.allocator,
"SELECT COUNT(*) FROM {s} WHERE min_x <= ? AND max_x >= ? AND min_y <= ? AND max_y >= ?\x00",
.{table_name},
);
defer self.allocator.free(sql);
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
defer stmt.finalize();
try stmt.bindFloat(1, box.max_x);
try stmt.bindFloat(2, box.min_x);
try stmt.bindFloat(3, box.max_y);
try stmt.bindFloat(4, box.min_y);
if (try stmt.step()) {
return stmt.columnInt(0);
}
return 0;
}
/// Gets the total count of entries in an R-Tree.
pub fn count(self: *Self, table_name: []const u8) !i64 {
const sql = try std.fmt.allocPrint(
self.allocator,
"SELECT COUNT(*) FROM {s}\x00",
.{table_name},
);
defer self.allocator.free(sql);
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
defer stmt.finalize();
if (try stmt.step()) {
return stmt.columnInt(0);
}
return 0;
}
/// Gets the bounding box that encompasses all entries in the R-Tree.
pub fn getBounds2D(self: *Self, table_name: []const u8) !?BoundingBox2D {
const sql = try std.fmt.allocPrint(
self.allocator,
"SELECT MIN(min_x), MAX(max_x), MIN(min_y), MAX(max_y) FROM {s}\x00",
.{table_name},
);
defer self.allocator.free(sql);
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
defer stmt.finalize();
if (try stmt.step()) {
if (stmt.columnIsNull(0)) return null;
return BoundingBox2D{
.min_x = stmt.columnFloat(0),
.max_x = stmt.columnFloat(1),
.min_y = stmt.columnFloat(2),
.max_y = stmt.columnFloat(3),
};
}
return null;
}
// ========================================================================
// Join Operations (R-Tree with regular tables)
// ========================================================================
/// Performs a spatial join between an R-Tree and a regular table.
/// Returns entries from both tables where the R-Tree bbox intersects the query box.
///
/// Example:
/// ```zig
/// var stmt = try rtree.spatialJoin2D(
/// "locations", // R-Tree table
/// "places", // Regular table
/// "id", // Join column in places
/// &.{ "name", "type" }, // Columns to select from places
/// query_box,
/// );
/// ```
pub fn spatialJoin2D(
self: *Self,
rtree_table: []const u8,
data_table: []const u8,
join_column: []const u8,
select_columns: []const []const u8,
box: BoundingBox2D,
) !Statement {
// Build select columns
var col_buf: [2048]u8 = undefined;
var col_len: usize = 0;
@memcpy(col_buf[col_len..][0..4], "r.id");
col_len += 4;
for (select_columns) |col| {
@memcpy(col_buf[col_len..][0..4], ", d.");
col_len += 4;
@memcpy(col_buf[col_len..][0..col.len], col);
col_len += col.len;
}
const sql = try std.fmt.allocPrint(
self.allocator,
"SELECT {s} FROM {s} r JOIN {s} d ON r.id = d.{s} WHERE r.min_x <= ? AND r.max_x >= ? AND r.min_y <= ? AND r.max_y >= ?\x00",
.{ col_buf[0..col_len], rtree_table, data_table, join_column },
);
defer self.allocator.free(sql);
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
errdefer stmt.finalize();
try stmt.bindFloat(1, box.max_x);
try stmt.bindFloat(2, box.min_x);
try stmt.bindFloat(3, box.max_y);
try stmt.bindFloat(4, box.min_y);
return stmt;
}
};

691
src/statement.zig Normal file
View file

@ -0,0 +1,691 @@
//! SQLite prepared statement handling
//!
//! Provides the Statement struct for executing SQL queries with parameter binding.
const std = @import("std");
const c = @import("c.zig").c;
const errors = @import("errors.zig");
const types = @import("types.zig");
const Error = errors.Error;
const resultToError = errors.resultToError;
const ColumnType = types.ColumnType;
// Forward declaration - Database will be imported where needed
pub const Database = @import("database.zig").Database;
/// Prepared SQL statement
pub const Statement = struct {
handle: ?*c.sqlite3_stmt,
db: *Database,
const Self = @This();
/// Finalizes (destroys) the statement.
pub fn finalize(self: *Self) void {
if (self.handle) |h| {
_ = c.sqlite3_finalize(h);
self.handle = null;
}
}
/// Resets the statement for re-execution.
pub fn reset(self: *Self) Error!void {
const result = c.sqlite3_reset(self.handle);
if (result != c.SQLITE_OK) {
return resultToError(result);
}
}
/// Clears all parameter bindings.
pub fn clearBindings(self: *Self) Error!void {
const result = c.sqlite3_clear_bindings(self.handle);
if (result != c.SQLITE_OK) {
return resultToError(result);
}
}
// ========================================================================
// Statement metadata
// ========================================================================
/// Returns the SQL text of the statement.
pub fn sql(self: *Self) ?[]const u8 {
const s = c.sqlite3_sql(self.handle);
if (s) |ptr| {
return std.mem.span(ptr);
}
return null;
}
/// Returns whether the statement is read-only.
pub fn isReadOnly(self: *Self) bool {
return c.sqlite3_stmt_readonly(self.handle) != 0;
}
/// Returns the number of parameters in the statement.
pub fn parameterCount(self: *Self) u32 {
return @intCast(c.sqlite3_bind_parameter_count(self.handle));
}
/// Returns the index of a named parameter.
/// Supports :name, @name, and $name styles.
/// Returns null if the parameter is not found.
pub fn parameterIndex(self: *Self, name: [:0]const u8) ?u32 {
const idx = c.sqlite3_bind_parameter_index(self.handle, name.ptr);
if (idx == 0) return null;
return @intCast(idx);
}
/// Returns the name of a parameter by index.
/// Returns null if the parameter has no name (positional ?).
pub fn parameterName(self: *Self, index: u32) ?[]const u8 {
const name = c.sqlite3_bind_parameter_name(self.handle, @intCast(index));
if (name) |n| {
return std.mem.span(n);
}
return null;
}
// ========================================================================
// Bind parameters (1-indexed as per SQLite convention)
// ========================================================================
/// Binds NULL to a parameter.
pub fn bindNull(self: *Self, index: u32) Error!void {
const result = c.sqlite3_bind_null(self.handle, @intCast(index));
if (result != c.SQLITE_OK) {
return resultToError(result);
}
}
/// Binds an integer to a parameter.
pub fn bindInt(self: *Self, index: u32, value: i64) Error!void {
const result = c.sqlite3_bind_int64(self.handle, @intCast(index), value);
if (result != c.SQLITE_OK) {
return resultToError(result);
}
}
/// Binds a float to a parameter.
pub fn bindFloat(self: *Self, index: u32, value: f64) Error!void {
const result = c.sqlite3_bind_double(self.handle, @intCast(index), value);
if (result != c.SQLITE_OK) {
return resultToError(result);
}
}
/// Binds text to a parameter. The text is copied by SQLite (SQLITE_TRANSIENT).
pub fn bindText(self: *Self, index: u32, value: []const u8) Error!void {
const result = c.sqlite3_bind_text(
self.handle,
@intCast(index),
value.ptr,
@intCast(value.len),
c.SQLITE_TRANSIENT,
);
if (result != c.SQLITE_OK) {
return resultToError(result);
}
}
/// Binds a blob to a parameter. The blob is copied by SQLite (SQLITE_TRANSIENT).
pub fn bindBlob(self: *Self, index: u32, value: []const u8) Error!void {
const result = c.sqlite3_bind_blob(
self.handle,
@intCast(index),
value.ptr,
@intCast(value.len),
c.SQLITE_TRANSIENT,
);
if (result != c.SQLITE_OK) {
return resultToError(result);
}
}
/// Binds a boolean to a parameter (as integer 0 or 1).
pub fn bindBool(self: *Self, index: u32, value: bool) Error!void {
try self.bindInt(index, if (value) 1 else 0);
}
/// Binds a zeroblob (a blob of zeros) to a parameter.
pub fn bindZeroblob(self: *Self, index: u32, size: u32) Error!void {
const result = c.sqlite3_bind_zeroblob(self.handle, @intCast(index), @intCast(size));
if (result != c.SQLITE_OK) {
return resultToError(result);
}
}
// ========================================================================
// Named parameter binding
// ========================================================================
/// Binds NULL to a named parameter.
pub fn bindNullNamed(self: *Self, name: [:0]const u8) Error!void {
const idx = self.parameterIndex(name) orelse return Error.Range;
try self.bindNull(idx);
}
/// Binds an integer to a named parameter.
pub fn bindIntNamed(self: *Self, name: [:0]const u8, value: i64) Error!void {
const idx = self.parameterIndex(name) orelse return Error.Range;
try self.bindInt(idx, value);
}
/// Binds a float to a named parameter.
pub fn bindFloatNamed(self: *Self, name: [:0]const u8, value: f64) Error!void {
const idx = self.parameterIndex(name) orelse return Error.Range;
try self.bindFloat(idx, value);
}
/// Binds text to a named parameter.
pub fn bindTextNamed(self: *Self, name: [:0]const u8, value: []const u8) Error!void {
const idx = self.parameterIndex(name) orelse return Error.Range;
try self.bindText(idx, value);
}
/// Binds a blob to a named parameter.
pub fn bindBlobNamed(self: *Self, name: [:0]const u8, value: []const u8) Error!void {
const idx = self.parameterIndex(name) orelse return Error.Range;
try self.bindBlob(idx, value);
}
/// Binds a boolean to a named parameter.
pub fn bindBoolNamed(self: *Self, name: [:0]const u8, value: bool) Error!void {
const idx = self.parameterIndex(name) orelse return Error.Range;
try self.bindBool(idx, value);
}
/// Binds a timestamp as ISO8601 text (YYYY-MM-DD HH:MM:SS).
pub fn bindTimestamp(self: *Self, index: u32, ts: i64) Error!void {
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_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
// ========================================================================
/// Executes one step of the statement.
/// Returns true if there's a row available (for SELECT statements).
/// Returns false when done (SQLITE_DONE).
pub fn step(self: *Self) Error!bool {
const result = c.sqlite3_step(self.handle);
return switch (result) {
c.SQLITE_ROW => true,
c.SQLITE_DONE => false,
else => resultToError(result),
};
}
// ========================================================================
// Column access (0-indexed as per SQLite convention)
// ========================================================================
/// Returns the number of columns in the result set.
pub fn columnCount(self: *Self) u32 {
return @intCast(c.sqlite3_column_count(self.handle));
}
/// Returns the name of a column.
pub fn columnName(self: *Self, index: u32) ?[]const u8 {
const name = c.sqlite3_column_name(self.handle, @intCast(index));
if (name) |n| {
return std.mem.span(n);
}
return null;
}
/// Returns the type of a column value.
pub fn columnType(self: *Self, index: u32) ColumnType {
const col_type = c.sqlite3_column_type(self.handle, @intCast(index));
return switch (col_type) {
c.SQLITE_INTEGER => .integer,
c.SQLITE_FLOAT => .float,
c.SQLITE_TEXT => .text,
c.SQLITE_BLOB => .blob,
c.SQLITE_NULL => .null_value,
else => .null_value,
};
}
/// Returns an integer column value.
pub fn columnInt(self: *Self, index: u32) i64 {
return c.sqlite3_column_int64(self.handle, @intCast(index));
}
/// Returns a float column value.
pub fn columnFloat(self: *Self, index: u32) f64 {
return c.sqlite3_column_double(self.handle, @intCast(index));
}
/// Returns a text column value.
pub fn columnText(self: *Self, index: u32) ?[]const u8 {
const len = c.sqlite3_column_bytes(self.handle, @intCast(index));
const text = c.sqlite3_column_text(self.handle, @intCast(index));
if (text) |t| {
return t[0..@intCast(len)];
}
return null;
}
/// Returns a blob column value.
pub fn columnBlob(self: *Self, index: u32) ?[]const u8 {
const len = c.sqlite3_column_bytes(self.handle, @intCast(index));
const blob = c.sqlite3_column_blob(self.handle, @intCast(index));
if (blob) |b| {
const ptr: [*]const u8 = @ptrCast(b);
return ptr[0..@intCast(len)];
}
return null;
}
/// Returns true if the column is NULL.
pub fn columnIsNull(self: *Self, index: u32) bool {
return self.columnType(index) == .null_value;
}
/// Returns a boolean column value (interprets 0 as false, non-zero as true).
pub fn columnBool(self: *Self, index: u32) bool {
return self.columnInt(index) != 0;
}
/// Returns the size in bytes of a blob or text column.
pub fn columnBytes(self: *Self, index: u32) u32 {
return @intCast(c.sqlite3_column_bytes(self.handle, @intCast(index)));
}
/// Returns the declared type of a column.
pub fn columnDeclType(self: *Self, index: u32) ?[]const u8 {
const dtype = c.sqlite3_column_decltype(self.handle, @intCast(index));
if (dtype) |d| {
return std.mem.span(d);
}
return null;
}
/// Returns the database name for a column result.
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.
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.
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.
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]);
c.sqlite3_free(expanded);
return result;
}
// ========================================================================
// Batch binding with tuples/structs
// ========================================================================
/// Binds all values from a tuple or struct to positional parameters.
/// Parameters are bound starting at index 1.
///
/// Example:
/// ```zig
/// var stmt = try db.prepare("INSERT INTO users (name, age, score) VALUES (?, ?, ?)");
/// try stmt.bindAll(.{ "Alice", @as(i64, 30), @as(f64, 95.5) });
/// ```
///
/// Supported types:
/// - `[]const u8`, `[:0]const u8` -> bindText
/// - `i64`, `i32`, `i16`, `i8`, `u32`, `u16`, `u8` -> bindInt
/// - `f64`, `f32` -> bindFloat
/// - `bool` -> bindBool
/// - `@TypeOf(null)` -> bindNull
/// - `?T` (optionals) -> bindNull if null, otherwise bind inner value
pub fn bindAll(self: *Self, values: anytype) Error!void {
const T = @TypeOf(values);
const info = @typeInfo(T);
if (info == .@"struct") {
const fields = info.@"struct".fields;
inline for (fields, 0..) |field, i| {
const value = @field(values, field.name);
try self.bindValue(@intCast(i + 1), value);
}
} else {
@compileError("bindAll expects a tuple or struct, got " ++ @typeName(T));
}
}
/// Binds a single value of any supported type to a parameter index.
pub fn bindValue(self: *Self, index: u32, value: anytype) Error!void {
const T = @TypeOf(value);
const info = @typeInfo(T);
// Handle optionals
if (info == .optional) {
if (value) |v| {
return self.bindValue(index, v);
} else {
return self.bindNull(index);
}
}
// Handle null type
if (T == @TypeOf(null)) {
return self.bindNull(index);
}
// Handle pointers to arrays (string literals)
if (info == .pointer) {
const child = info.pointer.child;
if (info.pointer.size == .slice) {
// []const u8 or []u8
if (child == u8) {
return self.bindText(index, value);
}
} else if (info.pointer.size == .one) {
// *const [N]u8 (string literal)
const child_info = @typeInfo(child);
if (child_info == .array and child_info.array.child == u8) {
return self.bindText(index, value);
}
}
}
// Handle arrays
if (info == .array and info.array.child == u8) {
return self.bindText(index, &value);
}
// Handle integers
if (info == .int or info == .comptime_int) {
return self.bindInt(index, @intCast(value));
}
// Handle floats
if (info == .float or info == .comptime_float) {
return self.bindFloat(index, @floatCast(value));
}
// Handle booleans
if (T == bool) {
return self.bindBool(index, value);
}
@compileError("Unsupported type for bindValue: " ++ @typeName(T));
}
/// Resets the statement and binds new values in one call.
/// Useful for executing the same statement multiple times with different values.
///
/// Example:
/// ```zig
/// var stmt = try db.prepare("INSERT INTO users (name, age) VALUES (?, ?)");
/// try stmt.rebind(.{ "Alice", @as(i64, 30) });
/// _ = try stmt.step();
/// try stmt.rebind(.{ "Bob", @as(i64, 25) });
/// _ = try stmt.step();
/// ```
pub fn rebind(self: *Self, values: anytype) Error!void {
try self.reset();
try self.clearBindings();
try self.bindAll(values);
}
// ========================================================================
// Row Iterator
// ========================================================================
/// Returns an iterator over result rows.
/// This provides a more idiomatic Zig interface for iterating over query results.
///
/// Example:
/// ```zig
/// var stmt = try db.prepare("SELECT id, name FROM users");
/// defer stmt.finalize();
///
/// var iter = stmt.iterator();
/// while (try iter.next()) |row| {
/// const id = row.int(0);
/// const name = row.text(1) orelse "(null)";
/// std.debug.print("User {}: {s}\n", .{ id, name });
/// }
/// ```
pub fn iterator(self: *Self) RowIterator {
return RowIterator{ .stmt = self };
}
/// Convenience method to iterate with a callback.
/// The callback receives a Row for each result row.
///
/// Example:
/// ```zig
/// try stmt.forEach(struct {
/// fn call(row: Row) void {
/// std.debug.print("{}: {s}\n", .{ row.int(0), row.text(1) orelse "" });
/// }
/// }.call);
/// ```
pub fn forEach(self: *Self, callback: *const fn (Row) void) Error!void {
while (try self.step()) {
callback(Row{ .stmt = self });
}
}
/// Executes the statement and collects all rows into a slice.
/// Each row is represented as an array of column values.
/// Caller owns the returned memory.
pub fn collectAll(self: *Self, allocator: std.mem.Allocator) ![]Row.Values {
var rows = std.ArrayList(Row.Values).init(allocator);
errdefer {
for (rows.items) |*r| r.deinit(allocator);
rows.deinit();
}
while (try self.step()) {
const row = Row{ .stmt = self };
try rows.append(try row.toValues(allocator));
}
return rows.toOwnedSlice();
}
};
/// Row iterator for idiomatic iteration over query results.
pub const RowIterator = struct {
stmt: *Statement,
const Self = @This();
/// Advances to the next row.
/// Returns a Row if there's data available, null if iteration is complete.
pub fn next(self: *Self) Error!?Row {
if (try self.stmt.step()) {
return Row{ .stmt = self.stmt };
}
return null;
}
/// Resets the iterator to the beginning.
pub fn reset(self: *Self) Error!void {
try self.stmt.reset();
}
};
/// Represents a single row in a query result.
/// Provides convenient access to column values.
pub const Row = struct {
stmt: *Statement,
const Self = @This();
/// Returns the number of columns.
pub fn columnCount(self: Self) u32 {
return self.stmt.columnCount();
}
/// Returns an integer column value.
pub fn int(self: Self, index: u32) i64 {
return self.stmt.columnInt(index);
}
/// Returns a float column value.
pub fn float(self: Self, index: u32) f64 {
return self.stmt.columnFloat(index);
}
/// Returns a text column value.
pub fn text(self: Self, index: u32) ?[]const u8 {
return self.stmt.columnText(index);
}
/// Returns a blob column value.
pub fn blob(self: Self, index: u32) ?[]const u8 {
return self.stmt.columnBlob(index);
}
/// Returns a boolean column value.
pub fn boolean(self: Self, index: u32) bool {
return self.stmt.columnBool(index);
}
/// Returns true if the column is NULL.
pub fn isNull(self: Self, index: u32) bool {
return self.stmt.columnIsNull(index);
}
/// Returns the column type.
pub fn columnType(self: Self, index: u32) ColumnType {
return self.stmt.columnType(index);
}
/// Returns the column name.
pub fn columnName(self: Self, index: u32) ?[]const u8 {
return self.stmt.columnName(index);
}
/// A value that can hold any SQLite column type.
pub const Value = union(ColumnType) {
integer: i64,
float: f64,
text: []const u8,
blob: []const u8,
null_value: void,
};
/// Owned values for a complete row.
pub const Values = struct {
items: []Value,
text_copies: [][]u8,
allocator: std.mem.Allocator,
pub fn deinit(self: *Values, allocator: std.mem.Allocator) void {
for (self.text_copies) |copy| {
allocator.free(copy);
}
allocator.free(self.text_copies);
allocator.free(self.items);
}
};
/// Converts the current row to owned Values.
/// Text and blob values are copied so they persist after iteration.
pub fn toValues(self: Self, allocator: std.mem.Allocator) !Values {
const count = self.columnCount();
const items = try allocator.alloc(Value, count);
errdefer allocator.free(items);
var text_copies = std.ArrayList([]u8).init(allocator);
errdefer {
for (text_copies.items) |copy| allocator.free(copy);
text_copies.deinit();
}
for (0..count) |i| {
const idx: u32 = @intCast(i);
const col_type = self.columnType(idx);
items[i] = switch (col_type) {
.integer => .{ .integer = self.int(idx) },
.float => .{ .float = self.float(idx) },
.text => blk: {
if (self.text(idx)) |t| {
const copy = try allocator.dupe(u8, t);
try text_copies.append(copy);
break :blk .{ .text = copy };
}
break :blk .{ .null_value = {} };
},
.blob => blk: {
if (self.blob(idx)) |b| {
const copy = try allocator.dupe(u8, b);
try text_copies.append(copy);
break :blk .{ .blob = copy };
}
break :blk .{ .null_value = {} };
},
.null_value => .{ .null_value = {} },
};
}
return Values{
.items = items,
.text_copies = try text_copies.toOwnedSlice(),
.allocator = allocator,
};
}
};

154
src/types.zig Normal file
View file

@ -0,0 +1,154 @@
//! Common types for zsqlite
//!
//! Contains enums, flags, and type definitions shared across modules.
const c = @import("c.zig").c;
/// Flags for opening a database connection
pub const OpenFlags = struct {
read_only: bool = false,
read_write: bool = true,
create: bool = true,
uri: bool = false,
memory: bool = false,
no_mutex: bool = false,
full_mutex: bool = false,
/// Converts flags to SQLite integer format
pub fn toInt(self: OpenFlags) c_int {
var flags: c_int = 0;
if (self.read_only) {
flags |= c.SQLITE_OPEN_READONLY;
} else if (self.read_write) {
flags |= c.SQLITE_OPEN_READWRITE;
if (self.create) {
flags |= c.SQLITE_OPEN_CREATE;
}
}
if (self.uri) flags |= c.SQLITE_OPEN_URI;
if (self.memory) flags |= c.SQLITE_OPEN_MEMORY;
if (self.no_mutex) flags |= c.SQLITE_OPEN_NOMUTEX;
if (self.full_mutex) flags |= c.SQLITE_OPEN_FULLMUTEX;
return flags;
}
};
/// SQLite column types
pub const ColumnType = enum(c_int) {
integer = 1,
float = 2,
text = 3,
blob = 4,
null_value = 5,
pub fn fromInt(value: c_int) ColumnType {
return switch (value) {
1 => .integer,
2 => .float,
3 => .text,
4 => .blob,
else => .null_value,
};
}
};
/// SQLite limit types for getLimit/setLimit
pub const Limit = enum(c_int) {
/// Maximum length of a string or BLOB
length = 0,
/// Maximum length of a SQL statement
sql_length = 1,
/// Maximum number of columns
column = 2,
/// Maximum depth of expression tree
expr_depth = 3,
/// Maximum number of terms in compound SELECT
compound_select = 4,
/// Maximum number of VDBE operations
vdbe_op = 5,
/// Maximum number of function arguments
function_arg = 6,
/// Maximum number of attached databases
attached = 7,
/// Maximum length of LIKE/GLOB pattern
like_pattern_length = 8,
/// Maximum number of bound parameters
variable_number = 9,
/// Maximum depth of trigger recursion
trigger_depth = 10,
/// Maximum number of worker threads
worker_threads = 11,
};
/// Operation type for update hooks
pub const UpdateOperation = enum(c_int) {
insert = 18, // SQLITE_INSERT
update = 23, // SQLITE_UPDATE
delete = 9, // SQLITE_DELETE
pub fn fromInt(value: c_int) ?UpdateOperation {
return switch (value) {
18 => .insert,
23 => .update,
9 => .delete,
else => null,
};
}
};
/// Authorization action codes
pub const AuthAction = enum(c_int) {
create_index = 1,
create_table = 2,
create_temp_index = 3,
create_temp_table = 4,
create_temp_trigger = 5,
create_temp_view = 6,
create_trigger = 7,
create_view = 8,
delete = 9,
drop_index = 10,
drop_table = 11,
drop_temp_index = 12,
drop_temp_table = 13,
drop_temp_trigger = 14,
drop_temp_view = 15,
drop_trigger = 16,
drop_view = 17,
insert = 18,
pragma = 19,
read = 20,
select = 21,
transaction = 22,
update = 23,
attach = 24,
detach = 25,
alter_table = 26,
reindex = 27,
analyze = 28,
create_vtable = 29,
drop_vtable = 30,
function = 31,
savepoint = 32,
recursive = 33,
pub fn fromInt(value: c_int) ?AuthAction {
if (value >= 1 and value <= 33) {
return @enumFromInt(value);
}
return null;
}
};
/// Authorization result codes
pub const AuthResult = enum(c_int) {
/// Allow the action
ok = 0,
/// Deny the action with an error
deny = 1,
/// Silently disallow (return NULL or skip)
ignore = 2,
};

321
src/vtable.zig Normal file
View file

@ -0,0 +1,321 @@
//! Virtual Table API
//!
//! Provides the foundation for creating custom SQLite virtual tables in Zig.
//! Virtual tables allow you to expose any data source (files, APIs, computed data)
//! as if it were a regular SQL table.
//!
//! This is an advanced feature - most users should use the higher-level helpers
//! like FTS5, JSON, or R-Tree modules instead.
const std = @import("std");
const c = @import("c.zig").c;
const Database = @import("database.zig").Database;
const errors = @import("errors.zig");
const Error = errors.Error;
const resultToError = errors.resultToError;
/// Index constraint operators.
pub const ConstraintOp = enum(u8) {
eq = 2, // SQLITE_INDEX_CONSTRAINT_EQ
gt = 4, // SQLITE_INDEX_CONSTRAINT_GT
le = 8, // SQLITE_INDEX_CONSTRAINT_LE
lt = 16, // SQLITE_INDEX_CONSTRAINT_LT
ge = 32, // SQLITE_INDEX_CONSTRAINT_GE
match = 64, // SQLITE_INDEX_CONSTRAINT_MATCH
like = 65, // SQLITE_INDEX_CONSTRAINT_LIKE
glob = 66, // SQLITE_INDEX_CONSTRAINT_GLOB
regexp = 67, // SQLITE_INDEX_CONSTRAINT_REGEXP
ne = 68, // SQLITE_INDEX_CONSTRAINT_NE
isnot = 69, // SQLITE_INDEX_CONSTRAINT_ISNOT
isnotnull = 70, // SQLITE_INDEX_CONSTRAINT_ISNOTNULL
isnull = 71, // SQLITE_INDEX_CONSTRAINT_ISNULL
is = 72, // SQLITE_INDEX_CONSTRAINT_IS
limit = 73, // SQLITE_INDEX_CONSTRAINT_LIMIT
offset = 74, // SQLITE_INDEX_CONSTRAINT_OFFSET
function = 150, // SQLITE_INDEX_CONSTRAINT_FUNCTION
};
/// Constraint information for query planning.
pub const IndexConstraint = struct {
column: i32,
op: ConstraintOp,
usable: bool,
};
/// Order by information for query planning.
pub const IndexOrderBy = struct {
column: i32,
desc: bool,
};
/// Index info passed to xBestIndex.
pub const IndexInfo = struct {
constraints: []IndexConstraint,
order_by: []IndexOrderBy,
// Output fields set by xBestIndex
idx_num: i32 = 0,
idx_str: ?[]const u8 = null,
order_by_consumed: bool = false,
estimated_cost: f64 = 1000000.0,
estimated_rows: i64 = 1000000,
};
/// Column type declaration for virtual table schema.
pub const ColumnDef = struct {
name: []const u8,
col_type: []const u8,
constraints: ?[]const u8 = null,
};
/// Virtual table module definition.
/// Implement this interface to create a custom virtual table.
pub fn VTableModule(comptime Context: type, comptime Cursor: type) type {
return struct {
/// Called when the virtual table is created.
/// Should return the column definitions for CREATE TABLE.
create_fn: *const fn (db: *Database, args: []const []const u8) anyerror!Context,
/// Called when the virtual table is connected (opened).
connect_fn: *const fn (db: *Database, args: []const []const u8) anyerror!Context,
/// Called to query the best index for a query.
best_index_fn: *const fn (ctx: *Context, info: *IndexInfo) anyerror!void,
/// Called when the virtual table is destroyed.
destroy_fn: *const fn (ctx: *Context) void,
/// Called to open a cursor for scanning.
open_fn: *const fn (ctx: *Context) anyerror!Cursor,
/// Called to close a cursor.
close_fn: *const fn (cursor: *Cursor) void,
/// Called to start a query with the chosen index.
filter_fn: *const fn (cursor: *Cursor, idx_num: i32, idx_str: ?[]const u8, args: []const ?*c.sqlite3_value) anyerror!void,
/// Called to advance to the next row.
next_fn: *const fn (cursor: *Cursor) anyerror!void,
/// Called to check if we've reached end of data.
eof_fn: *const fn (cursor: *Cursor) bool,
/// Called to get a column value.
column_fn: *const fn (cursor: *Cursor, col_idx: i32, result_ctx: *c.sqlite3_context) anyerror!void,
/// Called to get the rowid.
rowid_fn: *const fn (cursor: *Cursor) anyerror!i64,
/// Optional: Called to update a row.
update_fn: ?*const fn (ctx: *Context, argc: i32, argv: [*]?*c.sqlite3_value, rowid: *i64) anyerror!void = null,
/// Returns the schema for this virtual table.
schema_fn: *const fn (ctx: *Context) []const ColumnDef,
};
}
/// Simple read-only virtual table helper.
/// Easier to use than the full VTableModule for simple cases.
pub fn SimpleVTable(comptime Row: type) type {
return struct {
data: []const Row,
columns: []const ColumnDef,
const Self = @This();
pub fn init(data: []const Row, columns: []const ColumnDef) Self {
return .{
.data = data,
.columns = columns,
};
}
/// Cursor for iterating over rows.
pub const Cursor = struct {
table: *const Self,
current_row: usize,
pub fn next(self: *Cursor) void {
self.current_row += 1;
}
pub fn eof(self: *const Cursor) bool {
return self.current_row >= self.table.data.len;
}
pub fn currentRow(self: *const Cursor) ?Row {
if (self.eof()) return null;
return self.table.data[self.current_row];
}
pub fn rowid(self: *const Cursor) i64 {
return @intCast(self.current_row + 1);
}
};
pub fn openCursor(self: *const Self) Cursor {
return .{
.table = self,
.current_row = 0,
};
}
};
}
/// Generates CREATE TABLE statement for a virtual table schema.
pub fn generateSchema(allocator: std.mem.Allocator, table_name: []const u8, columns: []const ColumnDef) ![]u8 {
var sql = std.ArrayList(u8).init(allocator);
errdefer sql.deinit();
try sql.appendSlice("CREATE TABLE ");
try sql.appendSlice(table_name);
try sql.appendSlice("(");
for (columns, 0..) |col, i| {
if (i > 0) try sql.appendSlice(", ");
try sql.appendSlice(col.name);
try sql.append(' ');
try sql.appendSlice(col.col_type);
if (col.constraints) |cons| {
try sql.append(' ');
try sql.appendSlice(cons);
}
}
try sql.appendSlice(")");
return sql.toOwnedSlice();
}
/// Helper to set a result value in a sqlite3_context.
pub const ResultHelper = struct {
ctx: *c.sqlite3_context,
const Self = @This();
pub fn init(ctx: *c.sqlite3_context) Self {
return .{ .ctx = ctx };
}
pub fn setNull(self: Self) void {
c.sqlite3_result_null(self.ctx);
}
pub fn setInt(self: Self, value: i64) void {
c.sqlite3_result_int64(self.ctx, value);
}
pub fn setFloat(self: Self, value: f64) void {
c.sqlite3_result_double(self.ctx, value);
}
pub fn setText(self: Self, value: []const u8) void {
c.sqlite3_result_text(self.ctx, value.ptr, @intCast(value.len), c.SQLITE_TRANSIENT);
}
pub fn setBlob(self: Self, value: []const u8) void {
c.sqlite3_result_blob(self.ctx, value.ptr, @intCast(value.len), c.SQLITE_TRANSIENT);
}
pub fn setError(self: Self, msg: []const u8) void {
c.sqlite3_result_error(self.ctx, msg.ptr, @intCast(msg.len));
}
pub fn setErrorCode(self: Self, code: c_int) void {
c.sqlite3_result_error_code(self.ctx, code);
}
};
/// Helper to read values from sqlite3_value.
pub const ValueHelper = struct {
value: *c.sqlite3_value,
const Self = @This();
pub fn init(value: *c.sqlite3_value) Self {
return .{ .value = value };
}
pub fn getType(self: Self) c_int {
return c.sqlite3_value_type(self.value);
}
pub fn isNull(self: Self) bool {
return self.getType() == c.SQLITE_NULL;
}
pub fn getInt(self: Self) i64 {
return c.sqlite3_value_int64(self.value);
}
pub fn getFloat(self: Self) f64 {
return c.sqlite3_value_double(self.value);
}
pub fn getText(self: Self) ?[]const u8 {
const len = c.sqlite3_value_bytes(self.value);
const text = c.sqlite3_value_text(self.value);
if (text) |t| {
return t[0..@intCast(len)];
}
return null;
}
pub fn getBlob(self: Self) ?[]const u8 {
const len = c.sqlite3_value_bytes(self.value);
const blob = c.sqlite3_value_blob(self.value);
if (blob) |b| {
const ptr: [*]const u8 = @ptrCast(b);
return ptr[0..@intCast(len)];
}
return null;
}
};
/// Eponymous virtual table - appears automatically without CREATE VIRTUAL TABLE.
/// Useful for table-valued functions.
pub const EponymousModule = struct {
name: []const u8,
num_args: i32,
/// Generates SQL to query an eponymous module.
pub fn query(self: *const EponymousModule, allocator: std.mem.Allocator, args: []const []const u8) ![]u8 {
var sql = std.ArrayList(u8).init(allocator);
errdefer sql.deinit();
try sql.appendSlice("SELECT * FROM ");
try sql.appendSlice(self.name);
try sql.append('(');
for (args, 0..) |arg, i| {
if (i > 0) try sql.appendSlice(", ");
try sql.append('\'');
try sql.appendSlice(arg);
try sql.append('\'');
}
try sql.append(')');
return sql.toOwnedSlice();
}
};
// Note: Full virtual table registration requires complex C callback trampolines
// similar to what we do for UDFs. The helpers above provide the building blocks
// but the actual registration would need additional C interop code.
//
// For most use cases, the built-in virtual tables (FTS5, R-Tree, JSON) are
// sufficient. Custom virtual tables are an advanced feature that few applications
// need.
/// Common virtual table modules available in SQLite.
pub const BuiltinModules = struct {
/// FTS5 full-text search
pub const fts5 = "fts5";
/// R-Tree spatial index
pub const rtree = "rtree";
/// CSV virtual table (if compiled with)
pub const csv = "csv";
/// Generate series (built-in table-valued function)
pub const generate_series = "generate_series";
/// PRAGMA function as table
pub const pragma = "pragma";
};