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:
parent
7229c27c80
commit
5e28cbe4bf
10 changed files with 2603 additions and 3192 deletions
|
|
@ -331,13 +331,35 @@ while (try stmt.step()) {
|
||||||
|
|
||||||
## Decisiones de Diseno
|
## 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:
|
El codigo esta organizado en modulos especializados:
|
||||||
- `database.zig`
|
|
||||||
- `statement.zig`
|
```
|
||||||
- `errors.zig`
|
src/
|
||||||
- `types.zig`
|
├── 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
|
### 2. Error Union vs Nullable
|
||||||
|
|
||||||
|
|
@ -367,29 +389,15 @@ Esto facilita traducir ejemplos de documentacion SQLite.
|
||||||
|
|
||||||
## Roadmap Arquitectural
|
## Roadmap Arquitectural
|
||||||
|
|
||||||
### Fase 2: Modularizacion
|
### Completado: Modularizacion
|
||||||
|
|
||||||
Cuando el codigo crezca, fragmentar en modulos:
|
La estructura modular ya esta implementada (ver seccion "Estructura Modular").
|
||||||
```
|
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
### Fase 3: Features Avanzadas
|
### Siguiente: Optimizaciones
|
||||||
|
|
||||||
```
|
- [ ] Reducir duplicacion en funciones de binding
|
||||||
src/
|
- [ ] Crear helpers de test para reducir boilerplate
|
||||||
├── ...
|
- [ ] Considerar vtable API si se necesitan mas extensiones
|
||||||
├── blob.zig # Blob streaming
|
|
||||||
├── functions.zig # User-defined functions
|
|
||||||
├── backup.zig # Backup API
|
|
||||||
└── hooks.zig # Update/commit hooks
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
292
src/backup.zig
Normal file
292
src/backup.zig
Normal 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
24
src/c.zig
Normal 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
795
src/database.zig
Normal 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
142
src/errors.zig
Normal 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
567
src/functions.zig
Normal 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
151
src/pool.zig
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
3232
src/root.zig
3232
src/root.zig
File diff suppressed because it is too large
Load diff
378
src/statement.zig
Normal file
378
src/statement.zig
Normal 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
154
src/types.zig
Normal 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,
|
||||||
|
};
|
||||||
Loading…
Reference in a new issue