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>
This commit is contained in:
reugenio 2025-12-08 19:54:19 +01:00
parent 7229c27c80
commit 5e28cbe4bf
10 changed files with 2603 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;

795
src/database.zig Normal file
View file

@ -0,0 +1,795 @@
//! 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);
}
};

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",
};
}

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;
}

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

378
src/statement.zig Normal file
View file

@ -0,0 +1,378 @@
//! 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;
}
};

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,
};