Compare commits
No commits in common. "167e54530fdf1fb24fb63134a81f01f2efcab6b8" and "7229c27c8057ff24d8342acffa0621a778fd933c" have entirely different histories.
167e54530f
...
7229c27c80
14 changed files with 3183 additions and 4859 deletions
|
|
@ -331,35 +331,13 @@ while (try stmt.step()) {
|
|||
|
||||
## Decisiones de Diseno
|
||||
|
||||
### 1. Estructura Modular
|
||||
### 1. Todo en root.zig (por ahora)
|
||||
|
||||
El codigo esta organizado en modulos especializados:
|
||||
|
||||
```
|
||||
src/
|
||||
├── root.zig # Re-exports publicos + tests (~1100 lineas)
|
||||
├── c.zig # @cImport centralizado (24 lineas)
|
||||
├── errors.zig # Error enum y resultToError (142 lineas)
|
||||
├── types.zig # OpenFlags, ColumnType, enums (154 lineas)
|
||||
├── database.zig # Database struct (795 lineas)
|
||||
├── statement.zig # Statement struct (378 lineas)
|
||||
├── functions.zig # UDFs, hooks, callbacks (567 lineas)
|
||||
├── backup.zig # Backup y Blob I/O (292 lineas)
|
||||
└── pool.zig # ConnectionPool (151 lineas)
|
||||
```
|
||||
|
||||
**Total**: ~3600 lineas (vs 4200 monoliticas anteriores)
|
||||
|
||||
Cada modulo tiene una responsabilidad clara:
|
||||
- **c.zig**: Unico punto de @cImport para SQLite
|
||||
- **errors.zig**: Mapeo completo de errores SQLite -> Zig
|
||||
- **types.zig**: Tipos compartidos (flags, enums)
|
||||
- **database.zig**: Conexion y operaciones de base de datos
|
||||
- **statement.zig**: Prepared statements y bindings
|
||||
- **functions.zig**: Funciones definidas por usuario y hooks
|
||||
- **backup.zig**: API de backup y blob streaming
|
||||
- **pool.zig**: Pool de conexiones thread-safe
|
||||
- **root.zig**: Re-exporta API publica + contiene tests
|
||||
Para v0.1, todo el codigo esta en un solo archivo. Cuando crezca significativamente (>400 lineas core), se fragmentara en:
|
||||
- `database.zig`
|
||||
- `statement.zig`
|
||||
- `errors.zig`
|
||||
- `types.zig`
|
||||
|
||||
### 2. Error Union vs Nullable
|
||||
|
||||
|
|
@ -389,15 +367,29 @@ Esto facilita traducir ejemplos de documentacion SQLite.
|
|||
|
||||
## Roadmap Arquitectural
|
||||
|
||||
### Completado: Modularizacion
|
||||
### Fase 2: Modularizacion
|
||||
|
||||
La estructura modular ya esta implementada (ver seccion "Estructura Modular").
|
||||
Cuando el codigo crezca, fragmentar en modulos:
|
||||
```
|
||||
src/
|
||||
├── root.zig # Re-exports publicos
|
||||
├── database.zig # Database struct
|
||||
├── statement.zig # Statement struct
|
||||
├── errors.zig # Error types
|
||||
├── types.zig # OpenFlags, ColumnType
|
||||
└── c.zig # @cImport centralizado
|
||||
```
|
||||
|
||||
### Siguiente: Optimizaciones
|
||||
### Fase 3: Features Avanzadas
|
||||
|
||||
- [ ] Reducir duplicacion en funciones de binding
|
||||
- [ ] Crear helpers de test para reducir boilerplate
|
||||
- [ ] Considerar vtable API si se necesitan mas extensiones
|
||||
```
|
||||
src/
|
||||
├── ...
|
||||
├── blob.zig # Blob streaming
|
||||
├── functions.zig # User-defined functions
|
||||
├── backup.zig # Backup API
|
||||
└── hooks.zig # Update/commit hooks
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
292
src/backup.zig
292
src/backup.zig
|
|
@ -1,292 +0,0 @@
|
|||
//! 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
24
src/c.zig
|
|
@ -1,24 +0,0 @@
|
|||
//! SQLite C bindings
|
||||
//!
|
||||
//! Centralized @cImport for SQLite. All modules should import this
|
||||
//! instead of doing their own @cImport.
|
||||
|
||||
pub const c = @cImport({
|
||||
@cDefine("SQLITE_ENABLE_COLUMN_METADATA", "1");
|
||||
@cDefine("SQLITE_ENABLE_PREUPDATE_HOOK", "1");
|
||||
@cInclude("sqlite3.h");
|
||||
});
|
||||
|
||||
// Re-export commonly used types for convenience
|
||||
pub const sqlite3 = c.sqlite3;
|
||||
pub const sqlite3_stmt = c.sqlite3_stmt;
|
||||
pub const sqlite3_context = c.sqlite3_context;
|
||||
pub const sqlite3_value = c.sqlite3_value;
|
||||
pub const sqlite3_backup = c.sqlite3_backup;
|
||||
pub const sqlite3_blob = c.sqlite3_blob;
|
||||
|
||||
// Constants
|
||||
pub const SQLITE_OK = c.SQLITE_OK;
|
||||
pub const SQLITE_ROW = c.SQLITE_ROW;
|
||||
pub const SQLITE_DONE = c.SQLITE_DONE;
|
||||
pub const SQLITE_TRANSIENT = c.SQLITE_TRANSIENT;
|
||||
901
src/database.zig
901
src/database.zig
|
|
@ -1,901 +0,0 @@
|
|||
//! SQLite database connection
|
||||
//!
|
||||
//! Provides the main Database struct for managing SQLite connections.
|
||||
|
||||
const std = @import("std");
|
||||
const c = @import("c.zig").c;
|
||||
const errors = @import("errors.zig");
|
||||
const types = @import("types.zig");
|
||||
const functions = @import("functions.zig");
|
||||
const stmt_mod = @import("statement.zig");
|
||||
|
||||
const Error = errors.Error;
|
||||
const resultToError = errors.resultToError;
|
||||
const OpenFlags = types.OpenFlags;
|
||||
const Limit = types.Limit;
|
||||
const AuthAction = types.AuthAction;
|
||||
const AuthResult = types.AuthResult;
|
||||
const UpdateOperation = types.UpdateOperation;
|
||||
|
||||
// Re-export Statement for prepare() return type
|
||||
pub const Statement = stmt_mod.Statement;
|
||||
|
||||
// Function types
|
||||
const ScalarFn = functions.ScalarFn;
|
||||
const AggregateStepFn = functions.AggregateStepFn;
|
||||
const AggregateFinalFn = functions.AggregateFinalFn;
|
||||
const WindowValueFn = functions.WindowValueFn;
|
||||
const WindowInverseFn = functions.WindowInverseFn;
|
||||
const CollationFn = functions.CollationFn;
|
||||
const ZigCommitHookFn = functions.ZigCommitHookFn;
|
||||
const ZigRollbackHookFn = functions.ZigRollbackHookFn;
|
||||
const ZigUpdateHookFn = functions.ZigUpdateHookFn;
|
||||
const ZigPreUpdateHookFn = functions.ZigPreUpdateHookFn;
|
||||
const ZigAuthorizerFn = functions.ZigAuthorizerFn;
|
||||
const ZigProgressFn = functions.ZigProgressFn;
|
||||
const ZigBusyHandlerFn = functions.ZigBusyHandlerFn;
|
||||
|
||||
// Wrappers
|
||||
const ScalarFnWrapper = functions.ScalarFnWrapper;
|
||||
const AggregateFnWrapper = functions.AggregateFnWrapper;
|
||||
const WindowFnWrapper = functions.WindowFnWrapper;
|
||||
const CollationWrapper = functions.CollationWrapper;
|
||||
const CommitHookWrapper = functions.CommitHookWrapper;
|
||||
const RollbackHookWrapper = functions.RollbackHookWrapper;
|
||||
const UpdateHookWrapper = functions.UpdateHookWrapper;
|
||||
const PreUpdateHookWrapper = functions.PreUpdateHookWrapper;
|
||||
const AuthorizerWrapper = functions.AuthorizerWrapper;
|
||||
const ProgressWrapper = functions.ProgressWrapper;
|
||||
const BusyHandlerWrapper = functions.BusyHandlerWrapper;
|
||||
|
||||
/// SQLite database connection
|
||||
pub const Database = struct {
|
||||
handle: ?*c.sqlite3,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
// ========================================================================
|
||||
// Opening and Closing
|
||||
// ========================================================================
|
||||
|
||||
/// Opens a database connection.
|
||||
pub fn open(path: [:0]const u8) Error!Self {
|
||||
var handle: ?*c.sqlite3 = null;
|
||||
const result = c.sqlite3_open(path.ptr, &handle);
|
||||
|
||||
if (result != c.SQLITE_OK) {
|
||||
if (handle) |h| {
|
||||
_ = c.sqlite3_close(h);
|
||||
}
|
||||
return resultToError(result);
|
||||
}
|
||||
|
||||
return .{ .handle = handle };
|
||||
}
|
||||
|
||||
/// Opens a database with specific flags.
|
||||
pub fn openWithFlags(path: [:0]const u8, flags: OpenFlags) Error!Self {
|
||||
var handle: ?*c.sqlite3 = null;
|
||||
const result = c.sqlite3_open_v2(path.ptr, &handle, flags.toInt(), null);
|
||||
|
||||
if (result != c.SQLITE_OK) {
|
||||
if (handle) |h| {
|
||||
_ = c.sqlite3_close(h);
|
||||
}
|
||||
return resultToError(result);
|
||||
}
|
||||
|
||||
return .{ .handle = handle };
|
||||
}
|
||||
|
||||
/// Opens a database using a URI connection string.
|
||||
pub fn openUri(uri: [:0]const u8) Error!Self {
|
||||
var handle: ?*c.sqlite3 = null;
|
||||
const flags = c.SQLITE_OPEN_URI | c.SQLITE_OPEN_READWRITE | c.SQLITE_OPEN_CREATE;
|
||||
const result = c.sqlite3_open_v2(uri.ptr, &handle, flags, null);
|
||||
|
||||
if (result != c.SQLITE_OK) {
|
||||
if (handle) |h| {
|
||||
_ = c.sqlite3_close(h);
|
||||
}
|
||||
return resultToError(result);
|
||||
}
|
||||
|
||||
return .{ .handle = handle };
|
||||
}
|
||||
|
||||
/// Opens a database using a URI with an allocator for runtime strings.
|
||||
pub fn openUriAlloc(allocator: std.mem.Allocator, uri: []const u8) !Self {
|
||||
const uri_z = try allocator.dupeZ(u8, uri);
|
||||
defer allocator.free(uri_z);
|
||||
return Self.openUri(uri_z);
|
||||
}
|
||||
|
||||
/// Closes the database connection.
|
||||
pub fn close(self: *Self) void {
|
||||
if (self.handle) |h| {
|
||||
_ = c.sqlite3_close(h);
|
||||
self.handle = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// SQL Execution
|
||||
// ========================================================================
|
||||
|
||||
/// Executes SQL statement(s) without returning results.
|
||||
pub fn exec(self: *Self, sql: [:0]const u8) Error!void {
|
||||
const result = c.sqlite3_exec(self.handle, sql.ptr, null, null, null);
|
||||
if (result != c.SQLITE_OK) {
|
||||
return resultToError(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes SQL with runtime-known string.
|
||||
pub fn execAlloc(self: *Self, allocator: std.mem.Allocator, sql: []const u8) !void {
|
||||
const sql_z = try allocator.dupeZ(u8, sql);
|
||||
defer allocator.free(sql_z);
|
||||
try self.exec(sql_z);
|
||||
}
|
||||
|
||||
/// Prepares a SQL statement for execution.
|
||||
pub fn prepare(self: *Self, sql: [:0]const u8) Error!Statement {
|
||||
var stmt: ?*c.sqlite3_stmt = null;
|
||||
const result = c.sqlite3_prepare_v2(self.handle, sql.ptr, @intCast(sql.len + 1), &stmt, null);
|
||||
|
||||
if (result != c.SQLITE_OK) {
|
||||
return resultToError(result);
|
||||
}
|
||||
|
||||
return .{ .handle = stmt, .db = self };
|
||||
}
|
||||
|
||||
/// Prepares a SQL statement with runtime-known string.
|
||||
pub fn prepareAlloc(self: *Self, allocator: std.mem.Allocator, sql: []const u8) !Statement {
|
||||
const sql_z = try allocator.dupeZ(u8, sql);
|
||||
defer allocator.free(sql_z);
|
||||
return self.prepare(sql_z);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Database Info
|
||||
// ========================================================================
|
||||
|
||||
/// Returns the rowid of the most recent successful INSERT.
|
||||
pub fn lastInsertRowId(self: *Self) i64 {
|
||||
return c.sqlite3_last_insert_rowid(self.handle);
|
||||
}
|
||||
|
||||
/// Returns the number of rows modified by the most recent statement.
|
||||
pub fn changes(self: *Self) i32 {
|
||||
return c.sqlite3_changes(self.handle);
|
||||
}
|
||||
|
||||
/// Returns the total number of rows modified since connection opened.
|
||||
pub fn totalChanges(self: *Self) i32 {
|
||||
return c.sqlite3_total_changes(self.handle);
|
||||
}
|
||||
|
||||
/// Returns the most recent error message.
|
||||
pub fn errorMessage(self: *Self) ?[]const u8 {
|
||||
const msg = c.sqlite3_errmsg(self.handle);
|
||||
if (msg) |m| {
|
||||
return std.mem.span(m);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns the error code of the most recent error.
|
||||
pub fn errorCode(self: *Self) i32 {
|
||||
return c.sqlite3_errcode(self.handle);
|
||||
}
|
||||
|
||||
/// Returns the extended error code of the most recent error.
|
||||
pub fn extendedErrorCode(self: *Self) i32 {
|
||||
return c.sqlite3_extended_errcode(self.handle);
|
||||
}
|
||||
|
||||
/// Returns whether the database is read-only.
|
||||
pub fn isReadOnly(self: *Self, db_name: [:0]const u8) bool {
|
||||
return c.sqlite3_db_readonly(self.handle, db_name.ptr) == 1;
|
||||
}
|
||||
|
||||
/// Returns the filename of a database.
|
||||
pub fn filename(self: *Self, db_name: [:0]const u8) ?[]const u8 {
|
||||
const fname = c.sqlite3_db_filename(self.handle, db_name.ptr);
|
||||
if (fname) |f| {
|
||||
return std.mem.span(f);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Interrupts a long-running query.
|
||||
pub fn interrupt(self: *Self) void {
|
||||
c.sqlite3_interrupt(self.handle);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Transactions
|
||||
// ========================================================================
|
||||
|
||||
/// Enables or disables foreign key constraints.
|
||||
pub fn setForeignKeys(self: *Self, enabled: bool) Error!void {
|
||||
const sql: [:0]const u8 = if (enabled) "PRAGMA foreign_keys = ON" else "PRAGMA foreign_keys = OFF";
|
||||
try self.exec(sql);
|
||||
}
|
||||
|
||||
/// Begins a transaction.
|
||||
pub fn begin(self: *Self) Error!void {
|
||||
try self.exec("BEGIN");
|
||||
}
|
||||
|
||||
/// Begins an immediate transaction.
|
||||
pub fn beginImmediate(self: *Self) Error!void {
|
||||
try self.exec("BEGIN IMMEDIATE");
|
||||
}
|
||||
|
||||
/// Begins an exclusive transaction.
|
||||
pub fn beginExclusive(self: *Self) Error!void {
|
||||
try self.exec("BEGIN EXCLUSIVE");
|
||||
}
|
||||
|
||||
/// Commits the current transaction.
|
||||
pub fn commit(self: *Self) Error!void {
|
||||
try self.exec("COMMIT");
|
||||
}
|
||||
|
||||
/// Rolls back the current transaction.
|
||||
pub fn rollback(self: *Self) Error!void {
|
||||
try self.exec("ROLLBACK");
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Savepoints
|
||||
// ========================================================================
|
||||
|
||||
/// Creates a savepoint with the given name.
|
||||
pub fn savepoint(self: *Self, allocator: std.mem.Allocator, name: []const u8) !void {
|
||||
const sql = try std.fmt.allocPrint(allocator, "SAVEPOINT {s}\x00", .{name});
|
||||
defer allocator.free(sql);
|
||||
try self.exec(sql[0 .. sql.len - 1 :0]);
|
||||
}
|
||||
|
||||
/// Releases (commits) a savepoint.
|
||||
pub fn release(self: *Self, allocator: std.mem.Allocator, name: []const u8) !void {
|
||||
const sql = try std.fmt.allocPrint(allocator, "RELEASE SAVEPOINT {s}\x00", .{name});
|
||||
defer allocator.free(sql);
|
||||
try self.exec(sql[0 .. sql.len - 1 :0]);
|
||||
}
|
||||
|
||||
/// Rolls back to a savepoint.
|
||||
pub fn rollbackTo(self: *Self, allocator: std.mem.Allocator, name: []const u8) !void {
|
||||
const sql = try std.fmt.allocPrint(allocator, "ROLLBACK TO SAVEPOINT {s}\x00", .{name});
|
||||
defer allocator.free(sql);
|
||||
try self.exec(sql[0 .. sql.len - 1 :0]);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Pragmas and Configuration
|
||||
// ========================================================================
|
||||
|
||||
/// Sets the busy timeout in milliseconds.
|
||||
pub fn setBusyTimeout(self: *Self, ms: i32) Error!void {
|
||||
const result = c.sqlite3_busy_timeout(self.handle, ms);
|
||||
if (result != c.SQLITE_OK) {
|
||||
return resultToError(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the journal mode.
|
||||
pub fn setJournalMode(self: *Self, allocator: std.mem.Allocator, mode: []const u8) !void {
|
||||
const sql = try std.fmt.allocPrint(allocator, "PRAGMA journal_mode = {s}\x00", .{mode});
|
||||
defer allocator.free(sql);
|
||||
try self.exec(sql[0 .. sql.len - 1 :0]);
|
||||
}
|
||||
|
||||
/// Sets the synchronous mode.
|
||||
pub fn setSynchronous(self: *Self, allocator: std.mem.Allocator, mode: []const u8) !void {
|
||||
const sql = try std.fmt.allocPrint(allocator, "PRAGMA synchronous = {s}\x00", .{mode});
|
||||
defer allocator.free(sql);
|
||||
try self.exec(sql[0 .. sql.len - 1 :0]);
|
||||
}
|
||||
|
||||
/// Enables WAL mode with recommended settings.
|
||||
pub fn enableWalMode(self: *Self, allocator: std.mem.Allocator) !void {
|
||||
try self.setJournalMode(allocator, "WAL");
|
||||
try self.setSynchronous(allocator, "NORMAL");
|
||||
}
|
||||
|
||||
/// Sets the auto_vacuum mode.
|
||||
pub fn setAutoVacuum(self: *Self, allocator: std.mem.Allocator, mode: []const u8) !void {
|
||||
const sql = try std.fmt.allocPrint(allocator, "PRAGMA auto_vacuum = {s}\x00", .{mode});
|
||||
defer allocator.free(sql);
|
||||
try self.exec(sql[0 .. sql.len - 1 :0]);
|
||||
}
|
||||
|
||||
/// Sets the cache size.
|
||||
pub fn setCacheSize(self: *Self, allocator: std.mem.Allocator, size: i32) !void {
|
||||
const sql = try std.fmt.allocPrint(allocator, "PRAGMA cache_size = {d}\x00", .{size});
|
||||
defer allocator.free(sql);
|
||||
try self.exec(sql[0 .. sql.len - 1 :0]);
|
||||
}
|
||||
|
||||
/// Enables or disables case-sensitive LIKE.
|
||||
pub fn setCaseSensitiveLike(self: *Self, enabled: bool) Error!void {
|
||||
const sql: [:0]const u8 = if (enabled) "PRAGMA case_sensitive_like = ON" else "PRAGMA case_sensitive_like = OFF";
|
||||
try self.exec(sql);
|
||||
}
|
||||
|
||||
/// Enables or disables deferred foreign key enforcement.
|
||||
pub fn setDeferForeignKeys(self: *Self, enabled: bool) Error!void {
|
||||
const sql: [:0]const u8 = if (enabled) "PRAGMA defer_foreign_keys = ON" else "PRAGMA defer_foreign_keys = OFF";
|
||||
try self.exec(sql);
|
||||
}
|
||||
|
||||
/// Sets the locking mode.
|
||||
pub fn setLockingMode(self: *Self, allocator: std.mem.Allocator, mode: []const u8) !void {
|
||||
const sql = try std.fmt.allocPrint(allocator, "PRAGMA locking_mode = {s}\x00", .{mode});
|
||||
defer allocator.free(sql);
|
||||
try self.exec(sql[0 .. sql.len - 1 :0]);
|
||||
}
|
||||
|
||||
/// Enables or disables query_only mode.
|
||||
pub fn setQueryOnly(self: *Self, enabled: bool) Error!void {
|
||||
const sql: [:0]const u8 = if (enabled) "PRAGMA query_only = ON" else "PRAGMA query_only = OFF";
|
||||
try self.exec(sql);
|
||||
}
|
||||
|
||||
/// Enables or disables recursive triggers.
|
||||
pub fn setRecursiveTriggers(self: *Self, enabled: bool) Error!void {
|
||||
const sql: [:0]const u8 = if (enabled) "PRAGMA recursive_triggers = ON" else "PRAGMA recursive_triggers = OFF";
|
||||
try self.exec(sql);
|
||||
}
|
||||
|
||||
/// Enables or disables secure delete.
|
||||
pub fn setSecureDelete(self: *Self, enabled: bool) Error!void {
|
||||
const sql: [:0]const u8 = if (enabled) "PRAGMA secure_delete = ON" else "PRAGMA secure_delete = OFF";
|
||||
try self.exec(sql);
|
||||
}
|
||||
|
||||
/// Sets the page size.
|
||||
pub fn setPageSize(self: *Self, allocator: std.mem.Allocator, size: u32) !void {
|
||||
const sql = try std.fmt.allocPrint(allocator, "PRAGMA page_size = {d}\x00", .{size});
|
||||
defer allocator.free(sql);
|
||||
try self.exec(sql[0 .. sql.len - 1 :0]);
|
||||
}
|
||||
|
||||
/// Sets the maximum page count.
|
||||
pub fn setMaxPageCount(self: *Self, allocator: std.mem.Allocator, count: u32) !void {
|
||||
const sql = try std.fmt.allocPrint(allocator, "PRAGMA max_page_count = {d}\x00", .{count});
|
||||
defer allocator.free(sql);
|
||||
try self.exec(sql[0 .. sql.len - 1 :0]);
|
||||
}
|
||||
|
||||
/// Sets the temp_store location.
|
||||
pub fn setTempStore(self: *Self, allocator: std.mem.Allocator, mode: []const u8) !void {
|
||||
const sql = try std.fmt.allocPrint(allocator, "PRAGMA temp_store = {s}\x00", .{mode});
|
||||
defer allocator.free(sql);
|
||||
try self.exec(sql[0 .. sql.len - 1 :0]);
|
||||
}
|
||||
|
||||
/// Sets the WAL auto-checkpoint interval.
|
||||
pub fn setWalAutoCheckpoint(self: *Self, allocator: std.mem.Allocator, pages: u32) !void {
|
||||
const sql = try std.fmt.allocPrint(allocator, "PRAGMA wal_autocheckpoint = {d}\x00", .{pages});
|
||||
defer allocator.free(sql);
|
||||
try self.exec(sql[0 .. sql.len - 1 :0]);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Maintenance
|
||||
// ========================================================================
|
||||
|
||||
/// Runs integrity check and returns result.
|
||||
pub fn integrityCheck(self: *Self, allocator: std.mem.Allocator) ![]u8 {
|
||||
var stmt = try self.prepare("PRAGMA integrity_check");
|
||||
defer stmt.finalize();
|
||||
|
||||
if (try stmt.step()) {
|
||||
if (stmt.columnText(0)) |text| {
|
||||
return try allocator.dupe(u8, text);
|
||||
}
|
||||
}
|
||||
return try allocator.dupe(u8, "unknown");
|
||||
}
|
||||
|
||||
/// Runs quick integrity check.
|
||||
pub fn quickCheck(self: *Self, allocator: std.mem.Allocator) ![]u8 {
|
||||
var stmt = try self.prepare("PRAGMA quick_check");
|
||||
defer stmt.finalize();
|
||||
|
||||
if (try stmt.step()) {
|
||||
if (stmt.columnText(0)) |text| {
|
||||
return try allocator.dupe(u8, text);
|
||||
}
|
||||
}
|
||||
return try allocator.dupe(u8, "unknown");
|
||||
}
|
||||
|
||||
/// Runs VACUUM to rebuild the database file.
|
||||
pub fn vacuum(self: *Self) Error!void {
|
||||
try self.exec("VACUUM");
|
||||
}
|
||||
|
||||
/// Runs incremental vacuum.
|
||||
pub fn incrementalVacuum(self: *Self, allocator: std.mem.Allocator, pages: ?u32) !void {
|
||||
if (pages) |p| {
|
||||
const sql = try std.fmt.allocPrint(allocator, "PRAGMA incremental_vacuum({d})\x00", .{p});
|
||||
defer allocator.free(sql);
|
||||
try self.exec(sql[0 .. sql.len - 1 :0]);
|
||||
} else {
|
||||
try self.exec("PRAGMA incremental_vacuum");
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs a WAL checkpoint.
|
||||
pub fn walCheckpoint(self: *Self, allocator: std.mem.Allocator, mode: []const u8) !void {
|
||||
const sql = try std.fmt.allocPrint(allocator, "PRAGMA wal_checkpoint({s})\x00", .{mode});
|
||||
defer allocator.free(sql);
|
||||
try self.exec(sql[0 .. sql.len - 1 :0]);
|
||||
}
|
||||
|
||||
/// Optimizes the database.
|
||||
pub fn optimize(self: *Self) Error!void {
|
||||
try self.exec("PRAGMA optimize");
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// ATTACH/DETACH
|
||||
// ========================================================================
|
||||
|
||||
/// Attaches another database file to this connection.
|
||||
pub fn attach(self: *Self, allocator: std.mem.Allocator, file_path: []const u8, schema_name: []const u8) !void {
|
||||
const sql = try std.fmt.allocPrint(allocator, "ATTACH DATABASE '{s}' AS {s}\x00", .{ file_path, schema_name });
|
||||
defer allocator.free(sql);
|
||||
try self.exec(sql[0 .. sql.len - 1 :0]);
|
||||
}
|
||||
|
||||
/// Attaches an in-memory database to this connection.
|
||||
pub fn attachMemory(self: *Self, allocator: std.mem.Allocator, schema_name: []const u8) !void {
|
||||
const sql = try std.fmt.allocPrint(allocator, "ATTACH DATABASE ':memory:' AS {s}\x00", .{schema_name});
|
||||
defer allocator.free(sql);
|
||||
try self.exec(sql[0 .. sql.len - 1 :0]);
|
||||
}
|
||||
|
||||
/// Detaches a previously attached database.
|
||||
pub fn detach(self: *Self, allocator: std.mem.Allocator, schema_name: []const u8) !void {
|
||||
const sql = try std.fmt.allocPrint(allocator, "DETACH DATABASE {s}\x00", .{schema_name});
|
||||
defer allocator.free(sql);
|
||||
try self.exec(sql[0 .. sql.len - 1 :0]);
|
||||
}
|
||||
|
||||
/// Returns a list of attached database names.
|
||||
pub fn listDatabases(self: *Self, allocator: std.mem.Allocator) ![][]const u8 {
|
||||
var stmt = try self.prepare("PRAGMA database_list");
|
||||
defer stmt.finalize();
|
||||
|
||||
var list: std.ArrayListUnmanaged([]const u8) = .empty;
|
||||
errdefer {
|
||||
for (list.items) |item| allocator.free(item);
|
||||
list.deinit(allocator);
|
||||
}
|
||||
|
||||
while (try stmt.step()) {
|
||||
if (stmt.columnText(1)) |name| {
|
||||
const owned = try allocator.dupe(u8, name);
|
||||
try list.append(allocator, owned);
|
||||
}
|
||||
}
|
||||
|
||||
return list.toOwnedSlice(allocator);
|
||||
}
|
||||
|
||||
/// Frees the list returned by listDatabases.
|
||||
pub fn freeDatabaseList(allocator: std.mem.Allocator, list: [][]const u8) void {
|
||||
for (list) |item| allocator.free(item);
|
||||
allocator.free(list);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// User-Defined Functions
|
||||
// ========================================================================
|
||||
|
||||
/// Registers a scalar function with the database.
|
||||
pub fn createScalarFunction(self: *Self, name: [:0]const u8, num_args: i32, func: ScalarFn) !void {
|
||||
const wrapper = try ScalarFnWrapper.create(func);
|
||||
const result = c.sqlite3_create_function_v2(
|
||||
self.handle,
|
||||
name.ptr,
|
||||
num_args,
|
||||
c.SQLITE_UTF8,
|
||||
wrapper,
|
||||
functions.scalarCallback,
|
||||
null,
|
||||
null,
|
||||
functions.scalarDestructor,
|
||||
);
|
||||
|
||||
if (result != c.SQLITE_OK) {
|
||||
wrapper.destroy();
|
||||
return resultToError(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes a function from the database.
|
||||
pub fn removeFunction(self: *Self, name: [:0]const u8, num_args: i32) Error!void {
|
||||
const result = c.sqlite3_create_function_v2(
|
||||
self.handle,
|
||||
name.ptr,
|
||||
num_args,
|
||||
c.SQLITE_UTF8,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
if (result != c.SQLITE_OK) {
|
||||
return resultToError(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers an aggregate function with the database.
|
||||
pub fn createAggregateFunction(self: *Self, name: [:0]const u8, num_args: i32, step_fn: AggregateStepFn, final_fn: AggregateFinalFn) !void {
|
||||
const wrapper = try AggregateFnWrapper.create(step_fn, final_fn);
|
||||
const result = c.sqlite3_create_function_v2(
|
||||
self.handle,
|
||||
name.ptr,
|
||||
num_args,
|
||||
c.SQLITE_UTF8,
|
||||
wrapper,
|
||||
null,
|
||||
functions.aggregateStepCallback,
|
||||
functions.aggregateFinalCallback,
|
||||
functions.aggregateDestructor,
|
||||
);
|
||||
|
||||
if (result != c.SQLITE_OK) {
|
||||
wrapper.destroy();
|
||||
return resultToError(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers a window function with the database.
|
||||
pub fn createWindowFunction(
|
||||
self: *Self,
|
||||
name: [:0]const u8,
|
||||
num_args: i32,
|
||||
step_fn: AggregateStepFn,
|
||||
final_fn: AggregateFinalFn,
|
||||
value_fn: WindowValueFn,
|
||||
inverse_fn: WindowInverseFn,
|
||||
) !void {
|
||||
const wrapper = try WindowFnWrapper.create(step_fn, final_fn, value_fn, inverse_fn);
|
||||
const result = c.sqlite3_create_window_function(
|
||||
self.handle,
|
||||
name.ptr,
|
||||
num_args,
|
||||
c.SQLITE_UTF8,
|
||||
wrapper,
|
||||
functions.windowStepCallback,
|
||||
functions.windowFinalCallback,
|
||||
functions.windowValueCallback,
|
||||
functions.windowInverseCallback,
|
||||
functions.windowDestructor,
|
||||
);
|
||||
|
||||
if (result != c.SQLITE_OK) {
|
||||
wrapper.destroy();
|
||||
return resultToError(result);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Collations
|
||||
// ========================================================================
|
||||
|
||||
/// Registers a custom collation sequence.
|
||||
pub fn createCollation(self: *Self, name: [:0]const u8, func: CollationFn) !void {
|
||||
const wrapper = try CollationWrapper.create(func);
|
||||
const result = c.sqlite3_create_collation_v2(
|
||||
self.handle,
|
||||
name.ptr,
|
||||
c.SQLITE_UTF8,
|
||||
wrapper,
|
||||
functions.collationCallback,
|
||||
functions.collationDestructor,
|
||||
);
|
||||
|
||||
if (result != c.SQLITE_OK) {
|
||||
wrapper.destroy();
|
||||
return resultToError(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes a custom collation sequence.
|
||||
pub fn removeCollation(self: *Self, name: [:0]const u8) Error!void {
|
||||
const result = c.sqlite3_create_collation_v2(
|
||||
self.handle,
|
||||
name.ptr,
|
||||
c.SQLITE_UTF8,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
if (result != c.SQLITE_OK) {
|
||||
return resultToError(result);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Hooks
|
||||
// ========================================================================
|
||||
|
||||
/// Sets a commit hook callback.
|
||||
pub fn setCommitHook(self: *Self, func: ?ZigCommitHookFn) !void {
|
||||
if (func) |f| {
|
||||
const wrapper = try CommitHookWrapper.create(f);
|
||||
const old = c.sqlite3_commit_hook(self.handle, functions.commitHookCallback, wrapper);
|
||||
if (old != null) {
|
||||
const old_wrapper: *CommitHookWrapper = @ptrCast(@alignCast(old));
|
||||
old_wrapper.destroy();
|
||||
}
|
||||
} else {
|
||||
const old = c.sqlite3_commit_hook(self.handle, null, null);
|
||||
if (old != null) {
|
||||
const old_wrapper: *CommitHookWrapper = @ptrCast(@alignCast(old));
|
||||
old_wrapper.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets a rollback hook callback.
|
||||
pub fn setRollbackHook(self: *Self, func: ?ZigRollbackHookFn) !void {
|
||||
if (func) |f| {
|
||||
const wrapper = try RollbackHookWrapper.create(f);
|
||||
const old = c.sqlite3_rollback_hook(self.handle, functions.rollbackHookCallback, wrapper);
|
||||
if (old != null) {
|
||||
const old_wrapper: *RollbackHookWrapper = @ptrCast(@alignCast(old));
|
||||
old_wrapper.destroy();
|
||||
}
|
||||
} else {
|
||||
const old = c.sqlite3_rollback_hook(self.handle, null, null);
|
||||
if (old != null) {
|
||||
const old_wrapper: *RollbackHookWrapper = @ptrCast(@alignCast(old));
|
||||
old_wrapper.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets an update hook callback.
|
||||
pub fn setUpdateHook(self: *Self, func: ?ZigUpdateHookFn) !void {
|
||||
if (func) |f| {
|
||||
const wrapper = try UpdateHookWrapper.create(f);
|
||||
const old = c.sqlite3_update_hook(self.handle, functions.updateHookCallback, wrapper);
|
||||
if (old != null) {
|
||||
const old_wrapper: *UpdateHookWrapper = @ptrCast(@alignCast(old));
|
||||
old_wrapper.destroy();
|
||||
}
|
||||
} else {
|
||||
const old = c.sqlite3_update_hook(self.handle, null, null);
|
||||
if (old != null) {
|
||||
const old_wrapper: *UpdateHookWrapper = @ptrCast(@alignCast(old));
|
||||
old_wrapper.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets a pre-update hook callback.
|
||||
pub fn setPreUpdateHook(self: *Self, func: ?ZigPreUpdateHookFn) !void {
|
||||
if (func) |f| {
|
||||
const wrapper = try PreUpdateHookWrapper.create(f);
|
||||
const old = c.sqlite3_preupdate_hook(self.handle, functions.preUpdateHookCallback, wrapper);
|
||||
if (old != null) {
|
||||
const old_wrapper: *PreUpdateHookWrapper = @ptrCast(@alignCast(old));
|
||||
old_wrapper.destroy();
|
||||
}
|
||||
} else {
|
||||
const old = c.sqlite3_preupdate_hook(self.handle, null, null);
|
||||
if (old != null) {
|
||||
const old_wrapper: *PreUpdateHookWrapper = @ptrCast(@alignCast(old));
|
||||
old_wrapper.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes all hooks.
|
||||
pub fn clearHooks(self: *Self) void {
|
||||
const commit_old = c.sqlite3_commit_hook(self.handle, null, null);
|
||||
if (commit_old != null) {
|
||||
const wrapper: *CommitHookWrapper = @ptrCast(@alignCast(commit_old));
|
||||
wrapper.destroy();
|
||||
}
|
||||
|
||||
const rollback_old = c.sqlite3_rollback_hook(self.handle, null, null);
|
||||
if (rollback_old != null) {
|
||||
const wrapper: *RollbackHookWrapper = @ptrCast(@alignCast(rollback_old));
|
||||
wrapper.destroy();
|
||||
}
|
||||
|
||||
const update_old = c.sqlite3_update_hook(self.handle, null, null);
|
||||
if (update_old != null) {
|
||||
const wrapper: *UpdateHookWrapper = @ptrCast(@alignCast(update_old));
|
||||
wrapper.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Authorizer
|
||||
// ========================================================================
|
||||
|
||||
/// Sets an authorizer callback.
|
||||
pub fn setAuthorizer(self: *Self, func: ?ZigAuthorizerFn) !void {
|
||||
if (func) |f| {
|
||||
const wrapper = try AuthorizerWrapper.create(f);
|
||||
const result = c.sqlite3_set_authorizer(self.handle, functions.authorizerCallback, wrapper);
|
||||
if (result != c.SQLITE_OK) {
|
||||
wrapper.destroy();
|
||||
return resultToError(result);
|
||||
}
|
||||
} else {
|
||||
const result = c.sqlite3_set_authorizer(self.handle, null, null);
|
||||
if (result != c.SQLITE_OK) {
|
||||
return resultToError(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Progress Handler
|
||||
// ========================================================================
|
||||
|
||||
/// Sets a progress handler callback.
|
||||
pub fn setProgressHandler(self: *Self, n_ops: i32, func: ?ZigProgressFn) !void {
|
||||
if (func) |f| {
|
||||
const wrapper = try ProgressWrapper.create(f);
|
||||
c.sqlite3_progress_handler(self.handle, n_ops, functions.progressCallback, wrapper);
|
||||
} else {
|
||||
c.sqlite3_progress_handler(self.handle, 0, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Busy Handler
|
||||
// ========================================================================
|
||||
|
||||
/// Sets a custom busy handler callback.
|
||||
pub fn setBusyHandler(self: *Self, func: ?ZigBusyHandlerFn) !void {
|
||||
if (func) |f| {
|
||||
const wrapper = try BusyHandlerWrapper.create(f);
|
||||
const result = c.sqlite3_busy_handler(self.handle, functions.busyHandlerCallback, wrapper);
|
||||
if (result != c.SQLITE_OK) {
|
||||
wrapper.destroy();
|
||||
return resultToError(result);
|
||||
}
|
||||
} else {
|
||||
const result = c.sqlite3_busy_handler(self.handle, null, null);
|
||||
if (result != c.SQLITE_OK) {
|
||||
return resultToError(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Limits
|
||||
// ========================================================================
|
||||
|
||||
/// Gets the current value of a limit.
|
||||
pub fn getLimit(self: *Self, limit_type: Limit) i32 {
|
||||
return c.sqlite3_limit(self.handle, @intFromEnum(limit_type), -1);
|
||||
}
|
||||
|
||||
/// Sets a new value for a limit and returns the previous value.
|
||||
pub fn setLimit(self: *Self, limit_type: Limit, new_value: i32) i32 {
|
||||
return c.sqlite3_limit(self.handle, @intFromEnum(limit_type), new_value);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// File Control
|
||||
// ========================================================================
|
||||
|
||||
/// File control operation codes.
|
||||
pub const FileControlOp = enum(c_int) {
|
||||
lockstate = 1,
|
||||
get_lockproxyfile = 2,
|
||||
set_lockproxyfile = 3,
|
||||
last_errno = 4,
|
||||
size_hint = 5,
|
||||
chunk_size = 6,
|
||||
file_pointer = 7,
|
||||
sync_omitted = 8,
|
||||
win32_av_retry = 9,
|
||||
persist_wal = 10,
|
||||
overwrite = 11,
|
||||
vfsname = 12,
|
||||
powersafe_overwrite = 13,
|
||||
pragma = 14,
|
||||
busyhandler = 15,
|
||||
tempfilename = 16,
|
||||
mmap_size = 18,
|
||||
trace = 19,
|
||||
has_moved = 20,
|
||||
sync = 21,
|
||||
commit_phasetwo = 22,
|
||||
win32_set_handle = 23,
|
||||
wal_block = 24,
|
||||
zipvfs = 25,
|
||||
rbu = 26,
|
||||
vfs_pointer = 27,
|
||||
journal_pointer = 28,
|
||||
win32_get_handle = 29,
|
||||
pdb = 30,
|
||||
begin_atomic_write = 31,
|
||||
commit_atomic_write = 32,
|
||||
rollback_atomic_write = 33,
|
||||
lock_timeout = 34,
|
||||
data_version = 35,
|
||||
size_limit = 36,
|
||||
ckpt_done = 37,
|
||||
reserve_bytes = 38,
|
||||
ckpt_start = 39,
|
||||
external_reader = 40,
|
||||
cksm_file = 41,
|
||||
reset_cache = 42,
|
||||
};
|
||||
|
||||
/// Performs a file control operation with an integer value.
|
||||
///
|
||||
/// This is a low-level interface to the sqlite3_file_control() function.
|
||||
/// Most applications should use the higher-level pragma interface instead.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - db_name: The database name ("main", "temp", or attached db name)
|
||||
/// - op: The file control operation code
|
||||
/// - value: Pointer to the integer value (input/output depending on operation)
|
||||
///
|
||||
/// Returns error on failure.
|
||||
pub fn setFileControlInt(self: *Self, db_name: [:0]const u8, op: FileControlOp, value: *i32) Error!void {
|
||||
const result = c.sqlite3_file_control(self.handle, db_name.ptr, @intFromEnum(op), value);
|
||||
if (result != c.SQLITE_OK) {
|
||||
return resultToError(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets persist_wal setting.
|
||||
pub fn getPersistWal(self: *Self, db_name: [:0]const u8) Error!bool {
|
||||
var value: i32 = -1;
|
||||
try self.setFileControlInt(db_name, .persist_wal, &value);
|
||||
return value != 0;
|
||||
}
|
||||
|
||||
/// Sets persist_wal setting.
|
||||
pub fn setPersistWal(self: *Self, db_name: [:0]const u8, persist: bool) Error!void {
|
||||
var value: i32 = if (persist) 1 else 0;
|
||||
try self.setFileControlInt(db_name, .persist_wal, &value);
|
||||
}
|
||||
|
||||
/// Gets powersafe_overwrite setting.
|
||||
pub fn getPowersafeOverwrite(self: *Self, db_name: [:0]const u8) Error!bool {
|
||||
var value: i32 = -1;
|
||||
try self.setFileControlInt(db_name, .powersafe_overwrite, &value);
|
||||
return value != 0;
|
||||
}
|
||||
|
||||
/// Sets powersafe_overwrite setting.
|
||||
pub fn setPowersafeOverwrite(self: *Self, db_name: [:0]const u8, enabled: bool) Error!void {
|
||||
var value: i32 = if (enabled) 1 else 0;
|
||||
try self.setFileControlInt(db_name, .powersafe_overwrite, &value);
|
||||
}
|
||||
|
||||
/// Sets the chunk size for file growth.
|
||||
pub fn setChunkSize(self: *Self, db_name: [:0]const u8, size: i32) Error!void {
|
||||
var value: i32 = size;
|
||||
try self.setFileControlInt(db_name, .chunk_size, &value);
|
||||
}
|
||||
|
||||
/// Gets the data version (incremented each time the database is modified).
|
||||
pub fn getDataVersion(self: *Self, db_name: [:0]const u8) Error!u32 {
|
||||
var value: i32 = 0;
|
||||
try self.setFileControlInt(db_name, .data_version, &value);
|
||||
return @intCast(value);
|
||||
}
|
||||
};
|
||||
142
src/errors.zig
142
src/errors.zig
|
|
@ -1,142 +0,0 @@
|
|||
//! SQLite error handling
|
||||
//!
|
||||
//! Maps SQLite error codes to Zig errors for idiomatic error handling.
|
||||
|
||||
const c = @import("c.zig").c;
|
||||
|
||||
/// SQLite error codes mapped to Zig errors
|
||||
pub const Error = error{
|
||||
/// Generic error
|
||||
SqliteError,
|
||||
/// Internal logic error in SQLite
|
||||
InternalError,
|
||||
/// Access permission denied
|
||||
PermissionDenied,
|
||||
/// Callback routine requested an abort
|
||||
Abort,
|
||||
/// The database file is locked
|
||||
Busy,
|
||||
/// A table in the database is locked
|
||||
Locked,
|
||||
/// A malloc() failed
|
||||
OutOfMemory,
|
||||
/// Attempt to write a readonly database
|
||||
ReadOnly,
|
||||
/// Operation terminated by sqlite3_interrupt()
|
||||
Interrupt,
|
||||
/// Some kind of disk I/O error occurred
|
||||
IoError,
|
||||
/// The database disk image is malformed
|
||||
Corrupt,
|
||||
/// Unknown opcode in sqlite3_file_control()
|
||||
NotFound,
|
||||
/// Insertion failed because database is full
|
||||
Full,
|
||||
/// Unable to open the database file
|
||||
CantOpen,
|
||||
/// Database lock protocol error
|
||||
Protocol,
|
||||
/// Internal use only
|
||||
Empty,
|
||||
/// The database schema changed
|
||||
Schema,
|
||||
/// String or BLOB exceeds size limit
|
||||
TooBig,
|
||||
/// Abort due to constraint violation
|
||||
Constraint,
|
||||
/// Data type mismatch
|
||||
Mismatch,
|
||||
/// Library used incorrectly
|
||||
Misuse,
|
||||
/// Uses OS features not supported on host
|
||||
NoLfs,
|
||||
/// Authorization denied
|
||||
Auth,
|
||||
/// Not used
|
||||
Format,
|
||||
/// Parameter out of range
|
||||
Range,
|
||||
/// File opened that is not a database file
|
||||
NotADatabase,
|
||||
/// Notifications from sqlite3_log()
|
||||
Notice,
|
||||
/// Warnings from sqlite3_log()
|
||||
Warning,
|
||||
/// sqlite3_step() has another row ready
|
||||
Row,
|
||||
/// sqlite3_step() has finished executing
|
||||
Done,
|
||||
};
|
||||
|
||||
/// Converts a SQLite result code to a Zig error
|
||||
pub fn resultToError(result: c_int) Error {
|
||||
return switch (result) {
|
||||
c.SQLITE_ERROR => Error.SqliteError,
|
||||
c.SQLITE_INTERNAL => Error.InternalError,
|
||||
c.SQLITE_PERM => Error.PermissionDenied,
|
||||
c.SQLITE_ABORT => Error.Abort,
|
||||
c.SQLITE_BUSY => Error.Busy,
|
||||
c.SQLITE_LOCKED => Error.Locked,
|
||||
c.SQLITE_NOMEM => Error.OutOfMemory,
|
||||
c.SQLITE_READONLY => Error.ReadOnly,
|
||||
c.SQLITE_INTERRUPT => Error.Interrupt,
|
||||
c.SQLITE_IOERR => Error.IoError,
|
||||
c.SQLITE_CORRUPT => Error.Corrupt,
|
||||
c.SQLITE_NOTFOUND => Error.NotFound,
|
||||
c.SQLITE_FULL => Error.Full,
|
||||
c.SQLITE_CANTOPEN => Error.CantOpen,
|
||||
c.SQLITE_PROTOCOL => Error.Protocol,
|
||||
c.SQLITE_EMPTY => Error.Empty,
|
||||
c.SQLITE_SCHEMA => Error.Schema,
|
||||
c.SQLITE_TOOBIG => Error.TooBig,
|
||||
c.SQLITE_CONSTRAINT => Error.Constraint,
|
||||
c.SQLITE_MISMATCH => Error.Mismatch,
|
||||
c.SQLITE_MISUSE => Error.Misuse,
|
||||
c.SQLITE_NOLFS => Error.NoLfs,
|
||||
c.SQLITE_AUTH => Error.Auth,
|
||||
c.SQLITE_FORMAT => Error.Format,
|
||||
c.SQLITE_RANGE => Error.Range,
|
||||
c.SQLITE_NOTADB => Error.NotADatabase,
|
||||
c.SQLITE_NOTICE => Error.Notice,
|
||||
c.SQLITE_WARNING => Error.Warning,
|
||||
c.SQLITE_ROW => Error.Row,
|
||||
c.SQLITE_DONE => Error.Done,
|
||||
else => Error.SqliteError,
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns a human-readable description for an error
|
||||
pub fn errorDescription(err: Error) []const u8 {
|
||||
return switch (err) {
|
||||
Error.SqliteError => "Generic SQLite error",
|
||||
Error.InternalError => "Internal logic error in SQLite",
|
||||
Error.PermissionDenied => "Access permission denied",
|
||||
Error.Abort => "Callback routine requested an abort",
|
||||
Error.Busy => "The database file is locked",
|
||||
Error.Locked => "A table in the database is locked",
|
||||
Error.OutOfMemory => "A malloc() failed",
|
||||
Error.ReadOnly => "Attempt to write a readonly database",
|
||||
Error.Interrupt => "Operation terminated by sqlite3_interrupt()",
|
||||
Error.IoError => "Some kind of disk I/O error occurred",
|
||||
Error.Corrupt => "The database disk image is malformed",
|
||||
Error.NotFound => "Unknown opcode in sqlite3_file_control()",
|
||||
Error.Full => "Insertion failed because database is full",
|
||||
Error.CantOpen => "Unable to open the database file",
|
||||
Error.Protocol => "Database lock protocol error",
|
||||
Error.Empty => "Internal use only",
|
||||
Error.Schema => "The database schema changed",
|
||||
Error.TooBig => "String or BLOB exceeds size limit",
|
||||
Error.Constraint => "Abort due to constraint violation",
|
||||
Error.Mismatch => "Data type mismatch",
|
||||
Error.Misuse => "Library used incorrectly",
|
||||
Error.NoLfs => "Uses OS features not supported on host",
|
||||
Error.Auth => "Authorization denied",
|
||||
Error.Format => "Not used",
|
||||
Error.Range => "Parameter out of range",
|
||||
Error.NotADatabase => "File opened that is not a database file",
|
||||
Error.Notice => "Notifications from sqlite3_log()",
|
||||
Error.Warning => "Warnings from sqlite3_log()",
|
||||
Error.Row => "sqlite3_step() has another row ready",
|
||||
Error.Done => "sqlite3_step() has finished executing",
|
||||
};
|
||||
}
|
||||
229
src/fts5.zig
229
src/fts5.zig
|
|
@ -1,229 +0,0 @@
|
|||
//! FTS5 Full-Text Search Helpers
|
||||
//!
|
||||
//! Provides convenient wrappers for SQLite's FTS5 full-text search extension.
|
||||
//! FTS5 is compiled into SQLite by default with -DSQLITE_ENABLE_FTS5.
|
||||
|
||||
const std = @import("std");
|
||||
const Database = @import("database.zig").Database;
|
||||
const Statement = @import("statement.zig").Statement;
|
||||
const Error = @import("errors.zig").Error;
|
||||
|
||||
/// FTS5 tokenizer options.
|
||||
pub const TokenizerOption = enum {
|
||||
unicode61,
|
||||
ascii,
|
||||
porter,
|
||||
trigram,
|
||||
|
||||
pub fn name(self: TokenizerOption) []const u8 {
|
||||
return switch (self) {
|
||||
.unicode61 => "unicode61",
|
||||
.ascii => "ascii",
|
||||
.porter => "porter",
|
||||
.trigram => "trigram",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// FTS5 helper functions for a database connection.
|
||||
pub const Fts5 = struct {
|
||||
db: *Database,
|
||||
allocator: std.mem.Allocator,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
/// Creates a new FTS5 helper bound to a database connection.
|
||||
pub fn init(db: *Database, allocator: std.mem.Allocator) Self {
|
||||
return .{ .db = db, .allocator = allocator };
|
||||
}
|
||||
|
||||
/// Creates a simple FTS5 table with specified columns.
|
||||
pub fn createSimpleTable(self: *Self, table_name: []const u8, columns: []const []const u8) !void {
|
||||
try self.createTableWithTokenizer(table_name, columns, .unicode61);
|
||||
}
|
||||
|
||||
/// Creates an FTS5 table with a specific tokenizer.
|
||||
pub fn createTableWithTokenizer(
|
||||
self: *Self,
|
||||
table_name: []const u8,
|
||||
columns: []const []const u8,
|
||||
tokenizer: TokenizerOption,
|
||||
) !void {
|
||||
// Build column list
|
||||
var col_buf: [4096]u8 = undefined;
|
||||
var col_len: usize = 0;
|
||||
|
||||
for (columns, 0..) |col, i| {
|
||||
if (i > 0) {
|
||||
col_buf[col_len] = ',';
|
||||
col_buf[col_len + 1] = ' ';
|
||||
col_len += 2;
|
||||
}
|
||||
@memcpy(col_buf[col_len..][0..col.len], col);
|
||||
col_len += col.len;
|
||||
}
|
||||
|
||||
const sql = try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"CREATE VIRTUAL TABLE {s} USING fts5({s}, tokenize='{s}')\x00",
|
||||
.{ table_name, col_buf[0..col_len], tokenizer.name() },
|
||||
);
|
||||
defer self.allocator.free(sql);
|
||||
try self.db.exec(sql[0 .. sql.len - 1 :0]);
|
||||
}
|
||||
|
||||
/// Drops an FTS5 table.
|
||||
pub fn dropTable(self: *Self, table_name: []const u8) !void {
|
||||
const sql = try std.fmt.allocPrint(self.allocator, "DROP TABLE IF EXISTS {s}\x00", .{table_name});
|
||||
defer self.allocator.free(sql);
|
||||
try self.db.exec(sql[0 .. sql.len - 1 :0]);
|
||||
}
|
||||
|
||||
/// Performs a full-text search query.
|
||||
pub fn search(
|
||||
self: *Self,
|
||||
table_name: []const u8,
|
||||
query: []const u8,
|
||||
result_columns: []const []const u8,
|
||||
limit: ?u32,
|
||||
) !Statement {
|
||||
// Build column list
|
||||
var col_buf: [4096]u8 = undefined;
|
||||
var col_len: usize = 0;
|
||||
|
||||
if (result_columns.len == 0) {
|
||||
col_buf[0] = '*';
|
||||
col_len = 1;
|
||||
} else {
|
||||
for (result_columns, 0..) |col, i| {
|
||||
if (i > 0) {
|
||||
col_buf[col_len] = ',';
|
||||
col_buf[col_len + 1] = ' ';
|
||||
col_len += 2;
|
||||
}
|
||||
@memcpy(col_buf[col_len..][0..col.len], col);
|
||||
col_len += col.len;
|
||||
}
|
||||
}
|
||||
|
||||
const sql = if (limit) |l|
|
||||
try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"SELECT {s} FROM {s} WHERE {s} MATCH ? ORDER BY rank LIMIT {d}\x00",
|
||||
.{ col_buf[0..col_len], table_name, table_name, l },
|
||||
)
|
||||
else
|
||||
try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"SELECT {s} FROM {s} WHERE {s} MATCH ? ORDER BY rank\x00",
|
||||
.{ col_buf[0..col_len], table_name, table_name },
|
||||
);
|
||||
defer self.allocator.free(sql);
|
||||
|
||||
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
|
||||
try stmt.bindText(1, query);
|
||||
return stmt;
|
||||
}
|
||||
|
||||
/// Gets highlighted snippet from search results.
|
||||
pub fn searchWithHighlight(
|
||||
self: *Self,
|
||||
table_name: []const u8,
|
||||
query: []const u8,
|
||||
column_index: u32,
|
||||
before_match: []const u8,
|
||||
after_match: []const u8,
|
||||
limit: ?u32,
|
||||
) !Statement {
|
||||
const sql = if (limit) |l|
|
||||
try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"SELECT highlight({s}, {d}, '{s}', '{s}') FROM {s} WHERE {s} MATCH ? ORDER BY rank LIMIT {d}\x00",
|
||||
.{ table_name, column_index, before_match, after_match, table_name, table_name, l },
|
||||
)
|
||||
else
|
||||
try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"SELECT highlight({s}, {d}, '{s}', '{s}') FROM {s} WHERE {s} MATCH ? ORDER BY rank\x00",
|
||||
.{ table_name, column_index, before_match, after_match, table_name, table_name },
|
||||
);
|
||||
defer self.allocator.free(sql);
|
||||
|
||||
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
|
||||
try stmt.bindText(1, query);
|
||||
return stmt;
|
||||
}
|
||||
|
||||
/// Gets a snippet (context around matches) from search results.
|
||||
pub fn searchWithSnippet(
|
||||
self: *Self,
|
||||
table_name: []const u8,
|
||||
query: []const u8,
|
||||
column_index: i32,
|
||||
before_match: []const u8,
|
||||
after_match: []const u8,
|
||||
ellipsis: []const u8,
|
||||
max_tokens: u32,
|
||||
limit: ?u32,
|
||||
) !Statement {
|
||||
const sql = if (limit) |l|
|
||||
try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"SELECT snippet({s}, {d}, '{s}', '{s}', '{s}', {d}) FROM {s} WHERE {s} MATCH ? ORDER BY rank LIMIT {d}\x00",
|
||||
.{ table_name, column_index, before_match, after_match, ellipsis, max_tokens, table_name, table_name, l },
|
||||
)
|
||||
else
|
||||
try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"SELECT snippet({s}, {d}, '{s}', '{s}', '{s}', {d}) FROM {s} WHERE {s} MATCH ? ORDER BY rank\x00",
|
||||
.{ table_name, column_index, before_match, after_match, ellipsis, max_tokens, table_name, table_name },
|
||||
);
|
||||
defer self.allocator.free(sql);
|
||||
|
||||
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
|
||||
try stmt.bindText(1, query);
|
||||
return stmt;
|
||||
}
|
||||
|
||||
/// Rebuilds the FTS5 index.
|
||||
pub fn rebuild(self: *Self, table_name: []const u8) !void {
|
||||
const sql = try std.fmt.allocPrint(self.allocator, "INSERT INTO {s}({s}) VALUES('rebuild')\x00", .{ table_name, table_name });
|
||||
defer self.allocator.free(sql);
|
||||
try self.db.exec(sql[0 .. sql.len - 1 :0]);
|
||||
}
|
||||
|
||||
/// Optimizes the FTS5 index by merging segments.
|
||||
pub fn optimize(self: *Self, table_name: []const u8) !void {
|
||||
const sql = try std.fmt.allocPrint(self.allocator, "INSERT INTO {s}({s}) VALUES('optimize')\x00", .{ table_name, table_name });
|
||||
defer self.allocator.free(sql);
|
||||
try self.db.exec(sql[0 .. sql.len - 1 :0]);
|
||||
}
|
||||
|
||||
/// Runs integrity check on the FTS5 index.
|
||||
pub fn integrityCheck(self: *Self, table_name: []const u8) !void {
|
||||
const sql = try std.fmt.allocPrint(self.allocator, "INSERT INTO {s}({s}) VALUES('integrity-check')\x00", .{ table_name, table_name });
|
||||
defer self.allocator.free(sql);
|
||||
try self.db.exec(sql[0 .. sql.len - 1 :0]);
|
||||
}
|
||||
|
||||
/// Deletes all content from the FTS5 table.
|
||||
pub fn deleteAll(self: *Self, table_name: []const u8) !void {
|
||||
const sql = try std.fmt.allocPrint(self.allocator, "DELETE FROM {s}\x00", .{table_name});
|
||||
defer self.allocator.free(sql);
|
||||
try self.db.exec(sql[0 .. sql.len - 1 :0]);
|
||||
}
|
||||
|
||||
/// Gets the total number of rows in the FTS5 table.
|
||||
pub fn count(self: *Self, table_name: []const u8) !i64 {
|
||||
const sql = try std.fmt.allocPrint(self.allocator, "SELECT COUNT(*) FROM {s}\x00", .{table_name});
|
||||
defer self.allocator.free(sql);
|
||||
|
||||
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
|
||||
defer stmt.finalize();
|
||||
|
||||
if (try stmt.step()) {
|
||||
return stmt.columnInt(0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
|
@ -1,567 +0,0 @@
|
|||
//! User-defined functions and hooks for SQLite
|
||||
//!
|
||||
//! Provides support for scalar functions, aggregate functions, window functions,
|
||||
//! collations, and database hooks (commit, rollback, update, pre-update).
|
||||
|
||||
const std = @import("std");
|
||||
const c = @import("c.zig").c;
|
||||
const types = @import("types.zig");
|
||||
|
||||
const ColumnType = types.ColumnType;
|
||||
const UpdateOperation = types.UpdateOperation;
|
||||
const AuthAction = types.AuthAction;
|
||||
const AuthResult = types.AuthResult;
|
||||
|
||||
// ============================================================================
|
||||
// Function Context and Values
|
||||
// ============================================================================
|
||||
|
||||
/// Context for user-defined function results.
|
||||
pub const FunctionContext = struct {
|
||||
ctx: *c.sqlite3_context,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn setNull(self: Self) void {
|
||||
c.sqlite3_result_null(self.ctx);
|
||||
}
|
||||
|
||||
pub fn setInt(self: Self, value: i64) void {
|
||||
c.sqlite3_result_int64(self.ctx, value);
|
||||
}
|
||||
|
||||
pub fn setFloat(self: Self, value: f64) void {
|
||||
c.sqlite3_result_double(self.ctx, value);
|
||||
}
|
||||
|
||||
pub fn setText(self: Self, value: []const u8) void {
|
||||
c.sqlite3_result_text(self.ctx, value.ptr, @intCast(value.len), c.SQLITE_TRANSIENT);
|
||||
}
|
||||
|
||||
pub fn setBlob(self: Self, value: []const u8) void {
|
||||
c.sqlite3_result_blob(self.ctx, value.ptr, @intCast(value.len), c.SQLITE_TRANSIENT);
|
||||
}
|
||||
|
||||
pub fn setError(self: Self, msg: []const u8) void {
|
||||
c.sqlite3_result_error(self.ctx, msg.ptr, @intCast(msg.len));
|
||||
}
|
||||
};
|
||||
|
||||
/// Value passed to user-defined functions.
|
||||
pub const FunctionValue = struct {
|
||||
value: *c.sqlite3_value,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn getType(self: Self) ColumnType {
|
||||
const vtype = c.sqlite3_value_type(self.value);
|
||||
return switch (vtype) {
|
||||
c.SQLITE_INTEGER => .integer,
|
||||
c.SQLITE_FLOAT => .float,
|
||||
c.SQLITE_TEXT => .text,
|
||||
c.SQLITE_BLOB => .blob,
|
||||
c.SQLITE_NULL => .null_value,
|
||||
else => .null_value,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn isNull(self: Self) bool {
|
||||
return self.getType() == .null_value;
|
||||
}
|
||||
|
||||
pub fn asInt(self: Self) i64 {
|
||||
return c.sqlite3_value_int64(self.value);
|
||||
}
|
||||
|
||||
pub fn asFloat(self: Self) f64 {
|
||||
return c.sqlite3_value_double(self.value);
|
||||
}
|
||||
|
||||
pub fn asText(self: Self) ?[]const u8 {
|
||||
const len = c.sqlite3_value_bytes(self.value);
|
||||
const text = c.sqlite3_value_text(self.value);
|
||||
if (text) |t| {
|
||||
return t[0..@intCast(len)];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn asBlob(self: Self) ?[]const u8 {
|
||||
const len = c.sqlite3_value_bytes(self.value);
|
||||
const blob = c.sqlite3_value_blob(self.value);
|
||||
if (blob) |b| {
|
||||
const ptr: [*]const u8 = @ptrCast(b);
|
||||
return ptr[0..@intCast(len)];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/// Context for aggregate functions with state management.
|
||||
pub const AggregateContext = struct {
|
||||
ctx: *c.sqlite3_context,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn getAggregateContext(self: Self, comptime T: type) ?*T {
|
||||
const ptr = c.sqlite3_aggregate_context(self.ctx, @sizeOf(T));
|
||||
if (ptr == null) return null;
|
||||
return @ptrCast(@alignCast(ptr));
|
||||
}
|
||||
|
||||
pub fn setNull(self: Self) void {
|
||||
c.sqlite3_result_null(self.ctx);
|
||||
}
|
||||
|
||||
pub fn setInt(self: Self, value: i64) void {
|
||||
c.sqlite3_result_int64(self.ctx, value);
|
||||
}
|
||||
|
||||
pub fn setFloat(self: Self, value: f64) void {
|
||||
c.sqlite3_result_double(self.ctx, value);
|
||||
}
|
||||
|
||||
pub fn setText(self: Self, value: []const u8) void {
|
||||
c.sqlite3_result_text(self.ctx, value.ptr, @intCast(value.len), c.SQLITE_TRANSIENT);
|
||||
}
|
||||
|
||||
pub fn setBlob(self: Self, value: []const u8) void {
|
||||
c.sqlite3_result_blob(self.ctx, value.ptr, @intCast(value.len), c.SQLITE_TRANSIENT);
|
||||
}
|
||||
|
||||
pub fn setError(self: Self, msg: []const u8) void {
|
||||
c.sqlite3_result_error(self.ctx, msg.ptr, @intCast(msg.len));
|
||||
}
|
||||
};
|
||||
|
||||
/// Context for pre-update hook with access to old/new values.
|
||||
pub const PreUpdateContext = struct {
|
||||
db: *c.sqlite3,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn columnCount(self: Self) i32 {
|
||||
return c.sqlite3_preupdate_count(self.db);
|
||||
}
|
||||
|
||||
pub fn depth(self: Self) i32 {
|
||||
return c.sqlite3_preupdate_depth(self.db);
|
||||
}
|
||||
|
||||
pub fn oldValue(self: Self, col: u32) ?FunctionValue {
|
||||
var value: ?*c.sqlite3_value = null;
|
||||
const result = c.sqlite3_preupdate_old(self.db, @intCast(col), &value);
|
||||
if (result != c.SQLITE_OK or value == null) return null;
|
||||
return FunctionValue{ .value = value.? };
|
||||
}
|
||||
|
||||
pub fn newValue(self: Self, col: u32) ?FunctionValue {
|
||||
var value: ?*c.sqlite3_value = null;
|
||||
const result = c.sqlite3_preupdate_new(self.db, @intCast(col), &value);
|
||||
if (result != c.SQLITE_OK or value == null) return null;
|
||||
return FunctionValue{ .value = value.? };
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Function Types
|
||||
// ============================================================================
|
||||
|
||||
pub const ScalarFn = *const fn (ctx: FunctionContext, args: []const FunctionValue) void;
|
||||
pub const AggregateStepFn = *const fn (ctx: AggregateContext, args: []const FunctionValue) void;
|
||||
pub const AggregateFinalFn = *const fn (ctx: AggregateContext) void;
|
||||
pub const WindowValueFn = *const fn (ctx: AggregateContext) void;
|
||||
pub const WindowInverseFn = *const fn (ctx: AggregateContext, args: []const FunctionValue) void;
|
||||
pub const CollationFn = *const fn (a: []const u8, b: []const u8) i32;
|
||||
|
||||
// Hook function types
|
||||
pub const ZigCommitHookFn = *const fn () bool;
|
||||
pub const ZigRollbackHookFn = *const fn () void;
|
||||
pub const ZigUpdateHookFn = *const fn (operation: UpdateOperation, db_name: []const u8, table_name: []const u8, rowid: i64) void;
|
||||
pub const ZigPreUpdateHookFn = *const fn (ctx: PreUpdateContext, operation: UpdateOperation, db_name: []const u8, table_name: []const u8, old_rowid: i64, new_rowid: i64) void;
|
||||
pub const ZigAuthorizerFn = *const fn (action: AuthAction, arg1: ?[]const u8, arg2: ?[]const u8, arg3: ?[]const u8, arg4: ?[]const u8) AuthResult;
|
||||
pub const ZigProgressFn = *const fn () bool;
|
||||
pub const ZigBusyHandlerFn = *const fn (count: i32) bool;
|
||||
|
||||
// ============================================================================
|
||||
// Wrappers (stored in SQLite user_data)
|
||||
// ============================================================================
|
||||
|
||||
pub const ScalarFnWrapper = struct {
|
||||
func: ScalarFn,
|
||||
|
||||
pub fn create(func: ScalarFn) !*ScalarFnWrapper {
|
||||
const wrapper = try std.heap.page_allocator.create(ScalarFnWrapper);
|
||||
wrapper.func = func;
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
pub fn destroy(self: *ScalarFnWrapper) void {
|
||||
std.heap.page_allocator.destroy(self);
|
||||
}
|
||||
};
|
||||
|
||||
pub const AggregateFnWrapper = struct {
|
||||
step_fn: AggregateStepFn,
|
||||
final_fn: AggregateFinalFn,
|
||||
|
||||
pub fn create(step_fn: AggregateStepFn, final_fn: AggregateFinalFn) !*AggregateFnWrapper {
|
||||
const wrapper = try std.heap.page_allocator.create(AggregateFnWrapper);
|
||||
wrapper.step_fn = step_fn;
|
||||
wrapper.final_fn = final_fn;
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
pub fn destroy(self: *AggregateFnWrapper) void {
|
||||
std.heap.page_allocator.destroy(self);
|
||||
}
|
||||
};
|
||||
|
||||
pub const WindowFnWrapper = struct {
|
||||
step_fn: AggregateStepFn,
|
||||
final_fn: AggregateFinalFn,
|
||||
value_fn: WindowValueFn,
|
||||
inverse_fn: WindowInverseFn,
|
||||
|
||||
pub fn create(step_fn: AggregateStepFn, final_fn: AggregateFinalFn, value_fn: WindowValueFn, inverse_fn: WindowInverseFn) !*WindowFnWrapper {
|
||||
const wrapper = try std.heap.page_allocator.create(WindowFnWrapper);
|
||||
wrapper.step_fn = step_fn;
|
||||
wrapper.final_fn = final_fn;
|
||||
wrapper.value_fn = value_fn;
|
||||
wrapper.inverse_fn = inverse_fn;
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
pub fn destroy(self: *WindowFnWrapper) void {
|
||||
std.heap.page_allocator.destroy(self);
|
||||
}
|
||||
};
|
||||
|
||||
pub const CollationWrapper = struct {
|
||||
func: CollationFn,
|
||||
|
||||
pub fn create(func: CollationFn) !*CollationWrapper {
|
||||
const wrapper = try std.heap.page_allocator.create(CollationWrapper);
|
||||
wrapper.func = func;
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
pub fn destroy(self: *CollationWrapper) void {
|
||||
std.heap.page_allocator.destroy(self);
|
||||
}
|
||||
};
|
||||
|
||||
pub const CommitHookWrapper = struct {
|
||||
func: ZigCommitHookFn,
|
||||
|
||||
pub fn create(func: ZigCommitHookFn) !*CommitHookWrapper {
|
||||
const wrapper = try std.heap.page_allocator.create(CommitHookWrapper);
|
||||
wrapper.func = func;
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
pub fn destroy(self: *CommitHookWrapper) void {
|
||||
std.heap.page_allocator.destroy(self);
|
||||
}
|
||||
};
|
||||
|
||||
pub const RollbackHookWrapper = struct {
|
||||
func: ZigRollbackHookFn,
|
||||
|
||||
pub fn create(func: ZigRollbackHookFn) !*RollbackHookWrapper {
|
||||
const wrapper = try std.heap.page_allocator.create(RollbackHookWrapper);
|
||||
wrapper.func = func;
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
pub fn destroy(self: *RollbackHookWrapper) void {
|
||||
std.heap.page_allocator.destroy(self);
|
||||
}
|
||||
};
|
||||
|
||||
pub const UpdateHookWrapper = struct {
|
||||
func: ZigUpdateHookFn,
|
||||
|
||||
pub fn create(func: ZigUpdateHookFn) !*UpdateHookWrapper {
|
||||
const wrapper = try std.heap.page_allocator.create(UpdateHookWrapper);
|
||||
wrapper.func = func;
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
pub fn destroy(self: *UpdateHookWrapper) void {
|
||||
std.heap.page_allocator.destroy(self);
|
||||
}
|
||||
};
|
||||
|
||||
pub const PreUpdateHookWrapper = struct {
|
||||
func: ZigPreUpdateHookFn,
|
||||
|
||||
pub fn create(func: ZigPreUpdateHookFn) !*PreUpdateHookWrapper {
|
||||
const wrapper = try std.heap.page_allocator.create(PreUpdateHookWrapper);
|
||||
wrapper.func = func;
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
pub fn destroy(self: *PreUpdateHookWrapper) void {
|
||||
std.heap.page_allocator.destroy(self);
|
||||
}
|
||||
};
|
||||
|
||||
pub const AuthorizerWrapper = struct {
|
||||
func: ZigAuthorizerFn,
|
||||
|
||||
pub fn create(func: ZigAuthorizerFn) !*AuthorizerWrapper {
|
||||
const wrapper = try std.heap.page_allocator.create(AuthorizerWrapper);
|
||||
wrapper.func = func;
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
pub fn destroy(self: *AuthorizerWrapper) void {
|
||||
std.heap.page_allocator.destroy(self);
|
||||
}
|
||||
};
|
||||
|
||||
pub const ProgressWrapper = struct {
|
||||
func: ZigProgressFn,
|
||||
|
||||
pub fn create(func: ZigProgressFn) !*ProgressWrapper {
|
||||
const wrapper = try std.heap.page_allocator.create(ProgressWrapper);
|
||||
wrapper.func = func;
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
pub fn destroy(self: *ProgressWrapper) void {
|
||||
std.heap.page_allocator.destroy(self);
|
||||
}
|
||||
};
|
||||
|
||||
pub const BusyHandlerWrapper = struct {
|
||||
func: ZigBusyHandlerFn,
|
||||
|
||||
pub fn create(func: ZigBusyHandlerFn) !*BusyHandlerWrapper {
|
||||
const wrapper = try std.heap.page_allocator.create(BusyHandlerWrapper);
|
||||
wrapper.func = func;
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
pub fn destroy(self: *BusyHandlerWrapper) void {
|
||||
std.heap.page_allocator.destroy(self);
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// C Callback Trampolines
|
||||
// ============================================================================
|
||||
|
||||
pub fn scalarCallback(ctx: ?*c.sqlite3_context, argc: c_int, argv: [*c]?*c.sqlite3_value) callconv(.c) void {
|
||||
const user_data = c.sqlite3_user_data(ctx);
|
||||
if (user_data == null) return;
|
||||
|
||||
const wrapper: *ScalarFnWrapper = @ptrCast(@alignCast(user_data));
|
||||
const func_ctx = FunctionContext{ .ctx = ctx.? };
|
||||
|
||||
const args_count: usize = @intCast(argc);
|
||||
var args: [16]FunctionValue = undefined;
|
||||
const actual_count = @min(args_count, 16);
|
||||
|
||||
for (0..actual_count) |i| {
|
||||
if (argv[i]) |v| {
|
||||
args[i] = FunctionValue{ .value = v };
|
||||
}
|
||||
}
|
||||
|
||||
wrapper.func(func_ctx, args[0..actual_count]);
|
||||
}
|
||||
|
||||
pub fn scalarDestructor(ptr: ?*anyopaque) callconv(.c) void {
|
||||
if (ptr) |p| {
|
||||
const wrapper: *ScalarFnWrapper = @ptrCast(@alignCast(p));
|
||||
wrapper.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn aggregateStepCallback(ctx: ?*c.sqlite3_context, argc: c_int, argv: [*c]?*c.sqlite3_value) callconv(.c) void {
|
||||
const user_data = c.sqlite3_user_data(ctx);
|
||||
if (user_data == null) return;
|
||||
|
||||
const wrapper: *AggregateFnWrapper = @ptrCast(@alignCast(user_data));
|
||||
const agg_ctx = AggregateContext{ .ctx = ctx.? };
|
||||
|
||||
const args_count: usize = @intCast(argc);
|
||||
var args: [16]FunctionValue = undefined;
|
||||
const actual_count = @min(args_count, 16);
|
||||
|
||||
for (0..actual_count) |i| {
|
||||
if (argv[i]) |v| {
|
||||
args[i] = FunctionValue{ .value = v };
|
||||
}
|
||||
}
|
||||
|
||||
wrapper.step_fn(agg_ctx, args[0..actual_count]);
|
||||
}
|
||||
|
||||
pub fn aggregateFinalCallback(ctx: ?*c.sqlite3_context) callconv(.c) void {
|
||||
const user_data = c.sqlite3_user_data(ctx);
|
||||
if (user_data == null) return;
|
||||
|
||||
const wrapper: *AggregateFnWrapper = @ptrCast(@alignCast(user_data));
|
||||
const agg_ctx = AggregateContext{ .ctx = ctx.? };
|
||||
wrapper.final_fn(agg_ctx);
|
||||
}
|
||||
|
||||
pub fn aggregateDestructor(ptr: ?*anyopaque) callconv(.c) void {
|
||||
if (ptr) |p| {
|
||||
const wrapper: *AggregateFnWrapper = @ptrCast(@alignCast(p));
|
||||
wrapper.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn windowStepCallback(ctx: ?*c.sqlite3_context, argc: c_int, argv: [*c]?*c.sqlite3_value) callconv(.c) void {
|
||||
const user_data = c.sqlite3_user_data(ctx);
|
||||
if (user_data == null) return;
|
||||
|
||||
const wrapper: *WindowFnWrapper = @ptrCast(@alignCast(user_data));
|
||||
const agg_ctx = AggregateContext{ .ctx = ctx.? };
|
||||
|
||||
const args_count: usize = @intCast(argc);
|
||||
var args: [16]FunctionValue = undefined;
|
||||
const actual_count = @min(args_count, 16);
|
||||
|
||||
for (0..actual_count) |i| {
|
||||
if (argv[i]) |v| {
|
||||
args[i] = FunctionValue{ .value = v };
|
||||
}
|
||||
}
|
||||
|
||||
wrapper.step_fn(agg_ctx, args[0..actual_count]);
|
||||
}
|
||||
|
||||
pub fn windowFinalCallback(ctx: ?*c.sqlite3_context) callconv(.c) void {
|
||||
const user_data = c.sqlite3_user_data(ctx);
|
||||
if (user_data == null) return;
|
||||
|
||||
const wrapper: *WindowFnWrapper = @ptrCast(@alignCast(user_data));
|
||||
const agg_ctx = AggregateContext{ .ctx = ctx.? };
|
||||
wrapper.final_fn(agg_ctx);
|
||||
}
|
||||
|
||||
pub fn windowValueCallback(ctx: ?*c.sqlite3_context) callconv(.c) void {
|
||||
const user_data = c.sqlite3_user_data(ctx);
|
||||
if (user_data == null) return;
|
||||
|
||||
const wrapper: *WindowFnWrapper = @ptrCast(@alignCast(user_data));
|
||||
const agg_ctx = AggregateContext{ .ctx = ctx.? };
|
||||
wrapper.value_fn(agg_ctx);
|
||||
}
|
||||
|
||||
pub fn windowInverseCallback(ctx: ?*c.sqlite3_context, argc: c_int, argv: [*c]?*c.sqlite3_value) callconv(.c) void {
|
||||
const user_data = c.sqlite3_user_data(ctx);
|
||||
if (user_data == null) return;
|
||||
|
||||
const wrapper: *WindowFnWrapper = @ptrCast(@alignCast(user_data));
|
||||
const agg_ctx = AggregateContext{ .ctx = ctx.? };
|
||||
|
||||
const args_count: usize = @intCast(argc);
|
||||
var args: [16]FunctionValue = undefined;
|
||||
const actual_count = @min(args_count, 16);
|
||||
|
||||
for (0..actual_count) |i| {
|
||||
if (argv[i]) |v| {
|
||||
args[i] = FunctionValue{ .value = v };
|
||||
}
|
||||
}
|
||||
|
||||
wrapper.inverse_fn(agg_ctx, args[0..actual_count]);
|
||||
}
|
||||
|
||||
pub fn windowDestructor(ptr: ?*anyopaque) callconv(.c) void {
|
||||
if (ptr) |p| {
|
||||
const wrapper: *WindowFnWrapper = @ptrCast(@alignCast(p));
|
||||
wrapper.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn collationCallback(user_data: ?*anyopaque, len_a: c_int, data_a: ?*const anyopaque, len_b: c_int, data_b: ?*const anyopaque) callconv(.c) c_int {
|
||||
if (user_data == null) return 0;
|
||||
|
||||
const wrapper: *CollationWrapper = @ptrCast(@alignCast(user_data));
|
||||
|
||||
const a: []const u8 = if (data_a) |ptr|
|
||||
@as([*]const u8, @ptrCast(ptr))[0..@intCast(len_a)]
|
||||
else
|
||||
"";
|
||||
|
||||
const b: []const u8 = if (data_b) |ptr|
|
||||
@as([*]const u8, @ptrCast(ptr))[0..@intCast(len_b)]
|
||||
else
|
||||
"";
|
||||
|
||||
return wrapper.func(a, b);
|
||||
}
|
||||
|
||||
pub fn collationDestructor(ptr: ?*anyopaque) callconv(.c) void {
|
||||
if (ptr) |p| {
|
||||
const wrapper: *CollationWrapper = @ptrCast(@alignCast(p));
|
||||
wrapper.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn commitHookCallback(user_data: ?*anyopaque) callconv(.c) c_int {
|
||||
if (user_data == null) return 0;
|
||||
const wrapper: *CommitHookWrapper = @ptrCast(@alignCast(user_data));
|
||||
const allow_commit = wrapper.func();
|
||||
return if (allow_commit) 0 else 1;
|
||||
}
|
||||
|
||||
pub fn rollbackHookCallback(user_data: ?*anyopaque) callconv(.c) void {
|
||||
if (user_data == null) return;
|
||||
const wrapper: *RollbackHookWrapper = @ptrCast(@alignCast(user_data));
|
||||
wrapper.func();
|
||||
}
|
||||
|
||||
pub fn updateHookCallback(user_data: ?*anyopaque, operation: c_int, db_name: [*c]const u8, table_name: [*c]const u8, rowid: c.sqlite3_int64) callconv(.c) void {
|
||||
if (user_data == null) return;
|
||||
const wrapper: *UpdateHookWrapper = @ptrCast(@alignCast(user_data));
|
||||
const op = UpdateOperation.fromInt(operation) orelse return;
|
||||
const db_str = std.mem.span(db_name);
|
||||
const table_str = std.mem.span(table_name);
|
||||
wrapper.func(op, db_str, table_str, rowid);
|
||||
}
|
||||
|
||||
pub fn preUpdateHookCallback(user_data: ?*anyopaque, db: ?*c.sqlite3, operation: c_int, db_name: [*c]const u8, table_name: [*c]const u8, old_rowid: c.sqlite3_int64, new_rowid: c.sqlite3_int64) callconv(.c) void {
|
||||
if (user_data == null or db == null) return;
|
||||
const wrapper: *PreUpdateHookWrapper = @ptrCast(@alignCast(user_data));
|
||||
const op = UpdateOperation.fromInt(operation) orelse return;
|
||||
const db_str = std.mem.span(db_name);
|
||||
const table_str = std.mem.span(table_name);
|
||||
const ctx = PreUpdateContext{ .db = db.? };
|
||||
wrapper.func(ctx, op, db_str, table_str, old_rowid, new_rowid);
|
||||
}
|
||||
|
||||
pub fn authorizerCallback(user_data: ?*anyopaque, action: c_int, arg1: [*c]const u8, arg2: [*c]const u8, arg3: [*c]const u8, arg4: [*c]const u8) callconv(.c) c_int {
|
||||
if (user_data == null) return c.SQLITE_OK;
|
||||
const wrapper: *AuthorizerWrapper = @ptrCast(@alignCast(user_data));
|
||||
const auth_action = AuthAction.fromInt(action) orelse return c.SQLITE_OK;
|
||||
|
||||
const a1: ?[]const u8 = if (arg1 != null) std.mem.span(arg1) else null;
|
||||
const a2: ?[]const u8 = if (arg2 != null) std.mem.span(arg2) else null;
|
||||
const a3: ?[]const u8 = if (arg3 != null) std.mem.span(arg3) else null;
|
||||
const a4: ?[]const u8 = if (arg4 != null) std.mem.span(arg4) else null;
|
||||
|
||||
const result = wrapper.func(auth_action, a1, a2, a3, a4);
|
||||
return @intFromEnum(result);
|
||||
}
|
||||
|
||||
pub fn progressCallback(user_data: ?*anyopaque) callconv(.c) c_int {
|
||||
if (user_data == null) return 0;
|
||||
const wrapper: *ProgressWrapper = @ptrCast(@alignCast(user_data));
|
||||
const should_continue = wrapper.func();
|
||||
return if (should_continue) 0 else 1;
|
||||
}
|
||||
|
||||
pub fn busyHandlerCallback(user_data: ?*anyopaque, count: c_int) callconv(.c) c_int {
|
||||
if (user_data == null) return 0;
|
||||
const wrapper: *BusyHandlerWrapper = @ptrCast(@alignCast(user_data));
|
||||
const should_retry = wrapper.func(count);
|
||||
return if (should_retry) 1 else 0;
|
||||
}
|
||||
437
src/json.zig
437
src/json.zig
|
|
@ -1,437 +0,0 @@
|
|||
//! JSON1 Extension Helpers
|
||||
//!
|
||||
//! Provides convenient wrappers for SQLite's JSON1 extension functions.
|
||||
//! JSON1 is compiled into SQLite by default with -DSQLITE_ENABLE_JSON1.
|
||||
|
||||
const std = @import("std");
|
||||
const Database = @import("database.zig").Database;
|
||||
const Statement = @import("statement.zig").Statement;
|
||||
const Error = @import("errors.zig").Error;
|
||||
|
||||
/// JSON helper functions for a database connection.
|
||||
pub const Json = struct {
|
||||
db: *Database,
|
||||
allocator: std.mem.Allocator,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
/// Creates a new JSON helper bound to a database connection.
|
||||
pub fn init(db: *Database, allocator: std.mem.Allocator) Self {
|
||||
return .{ .db = db, .allocator = allocator };
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// JSON Validation and Parsing
|
||||
// ========================================================================
|
||||
|
||||
/// Validates and minifies a JSON string.
|
||||
pub fn validate(self: *Self, json_text: []const u8) !?[]u8 {
|
||||
var stmt = try self.db.prepare("SELECT json(?)");
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindText(1, json_text);
|
||||
|
||||
if (try stmt.step()) {
|
||||
if (stmt.columnText(0)) |text| {
|
||||
return try self.allocator.dupe(u8, text);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Checks if a string is valid JSON.
|
||||
pub fn isValid(self: *Self, json_text: []const u8) !bool {
|
||||
var stmt = try self.db.prepare("SELECT json_valid(?)");
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindText(1, json_text);
|
||||
|
||||
if (try stmt.step()) {
|
||||
return stmt.columnInt(0) == 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Returns the JSON type of a value at a path.
|
||||
pub fn getType(self: *Self, json_text: []const u8, path: []const u8) !?[]u8 {
|
||||
var stmt = try self.db.prepare("SELECT json_type(?, ?)");
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindText(1, json_text);
|
||||
try stmt.bindText(2, path);
|
||||
|
||||
if (try stmt.step()) {
|
||||
if (stmt.columnText(0)) |text| {
|
||||
return try self.allocator.dupe(u8, text);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// JSON Extraction
|
||||
// ========================================================================
|
||||
|
||||
/// Extracts a value from JSON at the given path.
|
||||
pub fn extract(self: *Self, json_text: []const u8, path: []const u8) !?[]u8 {
|
||||
var stmt = try self.db.prepare("SELECT json_extract(?, ?)");
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindText(1, json_text);
|
||||
try stmt.bindText(2, path);
|
||||
|
||||
if (try stmt.step()) {
|
||||
if (stmt.columnText(0)) |text| {
|
||||
return try self.allocator.dupe(u8, text);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Extracts an integer from JSON at the given path.
|
||||
pub fn extractInt(self: *Self, json_text: []const u8, path: []const u8) !?i64 {
|
||||
var stmt = try self.db.prepare("SELECT json_extract(?, ?)");
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindText(1, json_text);
|
||||
try stmt.bindText(2, path);
|
||||
|
||||
if (try stmt.step()) {
|
||||
if (!stmt.columnIsNull(0)) {
|
||||
return stmt.columnInt(0);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Extracts a float from JSON at the given path.
|
||||
pub fn extractFloat(self: *Self, json_text: []const u8, path: []const u8) !?f64 {
|
||||
var stmt = try self.db.prepare("SELECT json_extract(?, ?)");
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindText(1, json_text);
|
||||
try stmt.bindText(2, path);
|
||||
|
||||
if (try stmt.step()) {
|
||||
if (!stmt.columnIsNull(0)) {
|
||||
return stmt.columnFloat(0);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Extracts a boolean from JSON at the given path.
|
||||
pub fn extractBool(self: *Self, json_text: []const u8, path: []const u8) !?bool {
|
||||
const result = try self.extractInt(json_text, path);
|
||||
if (result) |v| {
|
||||
return v != 0;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// JSON Modification
|
||||
// ========================================================================
|
||||
|
||||
/// Inserts a value into JSON at the given path (only if path doesn't exist).
|
||||
pub fn insert(self: *Self, json_text: []const u8, path: []const u8, value: []const u8) ![]u8 {
|
||||
var stmt = try self.db.prepare("SELECT json_insert(?, ?, json(?))");
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindText(1, json_text);
|
||||
try stmt.bindText(2, path);
|
||||
try stmt.bindText(3, value);
|
||||
|
||||
if (try stmt.step()) {
|
||||
if (stmt.columnText(0)) |text| {
|
||||
return try self.allocator.dupe(u8, text);
|
||||
}
|
||||
}
|
||||
return try self.allocator.dupe(u8, json_text);
|
||||
}
|
||||
|
||||
/// Replaces a value in JSON at the given path (only if path exists).
|
||||
pub fn replace(self: *Self, json_text: []const u8, path: []const u8, value: []const u8) ![]u8 {
|
||||
var stmt = try self.db.prepare("SELECT json_replace(?, ?, json(?))");
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindText(1, json_text);
|
||||
try stmt.bindText(2, path);
|
||||
try stmt.bindText(3, value);
|
||||
|
||||
if (try stmt.step()) {
|
||||
if (stmt.columnText(0)) |text| {
|
||||
return try self.allocator.dupe(u8, text);
|
||||
}
|
||||
}
|
||||
return try self.allocator.dupe(u8, json_text);
|
||||
}
|
||||
|
||||
/// Sets a value in JSON at the given path (insert or replace).
|
||||
pub fn set(self: *Self, json_text: []const u8, path: []const u8, value: []const u8) ![]u8 {
|
||||
var stmt = try self.db.prepare("SELECT json_set(?, ?, json(?))");
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindText(1, json_text);
|
||||
try stmt.bindText(2, path);
|
||||
try stmt.bindText(3, value);
|
||||
|
||||
if (try stmt.step()) {
|
||||
if (stmt.columnText(0)) |text| {
|
||||
return try self.allocator.dupe(u8, text);
|
||||
}
|
||||
}
|
||||
return try self.allocator.dupe(u8, json_text);
|
||||
}
|
||||
|
||||
/// Sets a string value in JSON at the given path.
|
||||
pub fn setString(self: *Self, json_text: []const u8, path: []const u8, value: []const u8) ![]u8 {
|
||||
var stmt = try self.db.prepare("SELECT json_set(?, ?, ?)");
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindText(1, json_text);
|
||||
try stmt.bindText(2, path);
|
||||
try stmt.bindText(3, value);
|
||||
|
||||
if (try stmt.step()) {
|
||||
if (stmt.columnText(0)) |text| {
|
||||
return try self.allocator.dupe(u8, text);
|
||||
}
|
||||
}
|
||||
return try self.allocator.dupe(u8, json_text);
|
||||
}
|
||||
|
||||
/// Sets an integer value in JSON at the given path.
|
||||
pub fn setInt(self: *Self, json_text: []const u8, path: []const u8, value: i64) ![]u8 {
|
||||
var stmt = try self.db.prepare("SELECT json_set(?, ?, ?)");
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindText(1, json_text);
|
||||
try stmt.bindText(2, path);
|
||||
try stmt.bindInt(3, value);
|
||||
|
||||
if (try stmt.step()) {
|
||||
if (stmt.columnText(0)) |text| {
|
||||
return try self.allocator.dupe(u8, text);
|
||||
}
|
||||
}
|
||||
return try self.allocator.dupe(u8, json_text);
|
||||
}
|
||||
|
||||
/// Sets a float value in JSON at the given path.
|
||||
pub fn setFloat(self: *Self, json_text: []const u8, path: []const u8, value: f64) ![]u8 {
|
||||
var stmt = try self.db.prepare("SELECT json_set(?, ?, ?)");
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindText(1, json_text);
|
||||
try stmt.bindText(2, path);
|
||||
try stmt.bindFloat(3, value);
|
||||
|
||||
if (try stmt.step()) {
|
||||
if (stmt.columnText(0)) |text| {
|
||||
return try self.allocator.dupe(u8, text);
|
||||
}
|
||||
}
|
||||
return try self.allocator.dupe(u8, json_text);
|
||||
}
|
||||
|
||||
/// Sets a boolean value in JSON at the given path.
|
||||
pub fn setBool(self: *Self, json_text: []const u8, path: []const u8, value: bool) ![]u8 {
|
||||
const json_bool = if (value) "true" else "false";
|
||||
var stmt = try self.db.prepare("SELECT json_set(?, ?, json(?))");
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindText(1, json_text);
|
||||
try stmt.bindText(2, path);
|
||||
try stmt.bindText(3, json_bool);
|
||||
|
||||
if (try stmt.step()) {
|
||||
if (stmt.columnText(0)) |text| {
|
||||
return try self.allocator.dupe(u8, text);
|
||||
}
|
||||
}
|
||||
return try self.allocator.dupe(u8, json_text);
|
||||
}
|
||||
|
||||
/// Removes a value from JSON at the given path.
|
||||
pub fn remove(self: *Self, json_text: []const u8, path: []const u8) ![]u8 {
|
||||
var stmt = try self.db.prepare("SELECT json_remove(?, ?)");
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindText(1, json_text);
|
||||
try stmt.bindText(2, path);
|
||||
|
||||
if (try stmt.step()) {
|
||||
if (stmt.columnText(0)) |text| {
|
||||
return try self.allocator.dupe(u8, text);
|
||||
}
|
||||
}
|
||||
return try self.allocator.dupe(u8, json_text);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// JSON Array Operations
|
||||
// ========================================================================
|
||||
|
||||
/// Returns the length of a JSON array.
|
||||
pub fn arrayLength(self: *Self, json_text: []const u8, path: ?[]const u8) !?i64 {
|
||||
if (path) |p| {
|
||||
var stmt = try self.db.prepare("SELECT json_array_length(?, ?)");
|
||||
defer stmt.finalize();
|
||||
try stmt.bindText(1, json_text);
|
||||
try stmt.bindText(2, p);
|
||||
if (try stmt.step()) {
|
||||
if (!stmt.columnIsNull(0)) {
|
||||
return stmt.columnInt(0);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var stmt = try self.db.prepare("SELECT json_array_length(?)");
|
||||
defer stmt.finalize();
|
||||
try stmt.bindText(1, json_text);
|
||||
if (try stmt.step()) {
|
||||
if (!stmt.columnIsNull(0)) {
|
||||
return stmt.columnInt(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Creates a JSON array from string values.
|
||||
pub fn createArray(self: *Self, values: []const []const u8) ![]u8 {
|
||||
if (values.len == 0) {
|
||||
return try self.allocator.dupe(u8, "[]");
|
||||
}
|
||||
|
||||
// Build placeholders
|
||||
var placeholders: [256]u8 = undefined;
|
||||
var ph_len: usize = 0;
|
||||
for (0..values.len) |i| {
|
||||
if (i > 0) {
|
||||
placeholders[ph_len] = ',';
|
||||
placeholders[ph_len + 1] = ' ';
|
||||
ph_len += 2;
|
||||
}
|
||||
placeholders[ph_len] = '?';
|
||||
ph_len += 1;
|
||||
}
|
||||
|
||||
const sql = try std.fmt.allocPrint(self.allocator, "SELECT json_array({s})\x00", .{placeholders[0..ph_len]});
|
||||
defer self.allocator.free(sql);
|
||||
|
||||
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
|
||||
defer stmt.finalize();
|
||||
|
||||
for (values, 0..) |v, i| {
|
||||
try stmt.bindText(@intCast(i + 1), v);
|
||||
}
|
||||
|
||||
if (try stmt.step()) {
|
||||
if (stmt.columnText(0)) |text| {
|
||||
return try self.allocator.dupe(u8, text);
|
||||
}
|
||||
}
|
||||
return try self.allocator.dupe(u8, "[]");
|
||||
}
|
||||
|
||||
/// Creates a JSON object from key-value pairs.
|
||||
pub fn createObject(self: *Self, keys: []const []const u8, values: []const []const u8) ![]u8 {
|
||||
if (keys.len != values.len) {
|
||||
return error.OutOfMemory; // Use a generic error
|
||||
}
|
||||
|
||||
if (keys.len == 0) {
|
||||
return try self.allocator.dupe(u8, "{}");
|
||||
}
|
||||
|
||||
// Build placeholders
|
||||
var placeholders: [512]u8 = undefined;
|
||||
var ph_len: usize = 0;
|
||||
for (0..keys.len) |i| {
|
||||
if (i > 0) {
|
||||
placeholders[ph_len] = ',';
|
||||
placeholders[ph_len + 1] = ' ';
|
||||
ph_len += 2;
|
||||
}
|
||||
placeholders[ph_len] = '?';
|
||||
placeholders[ph_len + 1] = ',';
|
||||
placeholders[ph_len + 2] = ' ';
|
||||
placeholders[ph_len + 3] = '?';
|
||||
ph_len += 4;
|
||||
}
|
||||
|
||||
const sql = try std.fmt.allocPrint(self.allocator, "SELECT json_object({s})\x00", .{placeholders[0..ph_len]});
|
||||
defer self.allocator.free(sql);
|
||||
|
||||
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
|
||||
defer stmt.finalize();
|
||||
|
||||
var param_idx: u32 = 1;
|
||||
for (keys, values) |k, v| {
|
||||
try stmt.bindText(param_idx, k);
|
||||
param_idx += 1;
|
||||
try stmt.bindText(param_idx, v);
|
||||
param_idx += 1;
|
||||
}
|
||||
|
||||
if (try stmt.step()) {
|
||||
if (stmt.columnText(0)) |text| {
|
||||
return try self.allocator.dupe(u8, text);
|
||||
}
|
||||
}
|
||||
return try self.allocator.dupe(u8, "{}");
|
||||
}
|
||||
|
||||
/// Applies a JSON patch (RFC 7396) to a JSON document.
|
||||
pub fn patch(self: *Self, target: []const u8, patch_doc: []const u8) ![]u8 {
|
||||
var stmt = try self.db.prepare("SELECT json_patch(?, ?)");
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindText(1, target);
|
||||
try stmt.bindText(2, patch_doc);
|
||||
|
||||
if (try stmt.step()) {
|
||||
if (stmt.columnText(0)) |text| {
|
||||
return try self.allocator.dupe(u8, text);
|
||||
}
|
||||
}
|
||||
return try self.allocator.dupe(u8, target);
|
||||
}
|
||||
|
||||
/// Returns a statement for iterating over JSON elements with json_each.
|
||||
pub fn each(self: *Self, json_text: []const u8, path: ?[]const u8) !Statement {
|
||||
if (path) |p| {
|
||||
var stmt = try self.db.prepare(
|
||||
"SELECT key, value, type, atom, id, parent, fullkey, path FROM json_each(?, ?)",
|
||||
);
|
||||
try stmt.bindText(1, json_text);
|
||||
try stmt.bindText(2, p);
|
||||
return stmt;
|
||||
} else {
|
||||
var stmt = try self.db.prepare(
|
||||
"SELECT key, value, type, atom, id, parent, fullkey, path FROM json_each(?)",
|
||||
);
|
||||
try stmt.bindText(1, json_text);
|
||||
return stmt;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a statement for iterating over all JSON elements with json_tree.
|
||||
pub fn tree(self: *Self, json_text: []const u8, path: ?[]const u8) !Statement {
|
||||
if (path) |p| {
|
||||
var stmt = try self.db.prepare(
|
||||
"SELECT key, value, type, atom, id, parent, fullkey, path FROM json_tree(?, ?)",
|
||||
);
|
||||
try stmt.bindText(1, json_text);
|
||||
try stmt.bindText(2, p);
|
||||
return stmt;
|
||||
} else {
|
||||
var stmt = try self.db.prepare(
|
||||
"SELECT key, value, type, atom, id, parent, fullkey, path FROM json_tree(?)",
|
||||
);
|
||||
try stmt.bindText(1, json_text);
|
||||
return stmt;
|
||||
}
|
||||
}
|
||||
};
|
||||
151
src/pool.zig
151
src/pool.zig
|
|
@ -1,151 +0,0 @@
|
|||
//! 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;
|
||||
}
|
||||
};
|
||||
3509
src/root.zig
3509
src/root.zig
File diff suppressed because it is too large
Load diff
564
src/rtree.zig
564
src/rtree.zig
|
|
@ -1,564 +0,0 @@
|
|||
//! R-Tree Spatial Index Helpers
|
||||
//!
|
||||
//! Provides convenient wrappers for SQLite's R-Tree extension for spatial indexing.
|
||||
//! R-Tree is compiled into SQLite by default with -DSQLITE_ENABLE_RTREE.
|
||||
//!
|
||||
//! R-Tree is useful for:
|
||||
//! - Geographic data (latitude/longitude bounding boxes)
|
||||
//! - 2D/3D spatial queries
|
||||
//! - Range queries on multiple dimensions
|
||||
//! - Game collision detection
|
||||
//! - Any multi-dimensional range search
|
||||
|
||||
const std = @import("std");
|
||||
const Database = @import("database.zig").Database;
|
||||
const Statement = @import("statement.zig").Statement;
|
||||
const Error = @import("errors.zig").Error;
|
||||
|
||||
/// A 2D bounding box (rectangle).
|
||||
pub const BoundingBox2D = struct {
|
||||
min_x: f64,
|
||||
max_x: f64,
|
||||
min_y: f64,
|
||||
max_y: f64,
|
||||
|
||||
/// Creates a bounding box from center point and size.
|
||||
pub fn fromCenter(center_x: f64, center_y: f64, width: f64, height: f64) BoundingBox2D {
|
||||
const half_w = width / 2.0;
|
||||
const half_h = height / 2.0;
|
||||
return .{
|
||||
.min_x = center_x - half_w,
|
||||
.max_x = center_x + half_w,
|
||||
.min_y = center_y - half_h,
|
||||
.max_y = center_y + half_h,
|
||||
};
|
||||
}
|
||||
|
||||
/// Creates a bounding box from a point (zero-size box).
|
||||
pub fn fromPoint(x: f64, y: f64) BoundingBox2D {
|
||||
return .{
|
||||
.min_x = x,
|
||||
.max_x = x,
|
||||
.min_y = y,
|
||||
.max_y = y,
|
||||
};
|
||||
}
|
||||
|
||||
/// Checks if this box intersects with another box.
|
||||
pub fn intersects(self: BoundingBox2D, other: BoundingBox2D) bool {
|
||||
return self.min_x <= other.max_x and
|
||||
self.max_x >= other.min_x and
|
||||
self.min_y <= other.max_y and
|
||||
self.max_y >= other.min_y;
|
||||
}
|
||||
|
||||
/// Checks if this box contains a point.
|
||||
pub fn containsPoint(self: BoundingBox2D, x: f64, y: f64) bool {
|
||||
return x >= self.min_x and x <= self.max_x and
|
||||
y >= self.min_y and y <= self.max_y;
|
||||
}
|
||||
|
||||
/// Checks if this box fully contains another box.
|
||||
pub fn contains(self: BoundingBox2D, other: BoundingBox2D) bool {
|
||||
return other.min_x >= self.min_x and
|
||||
other.max_x <= self.max_x and
|
||||
other.min_y >= self.min_y and
|
||||
other.max_y <= self.max_y;
|
||||
}
|
||||
|
||||
/// Returns the area of the bounding box.
|
||||
pub fn area(self: BoundingBox2D) f64 {
|
||||
return (self.max_x - self.min_x) * (self.max_y - self.min_y);
|
||||
}
|
||||
|
||||
/// Returns the center point of the bounding box.
|
||||
pub fn center(self: BoundingBox2D) struct { x: f64, y: f64 } {
|
||||
return .{
|
||||
.x = (self.min_x + self.max_x) / 2.0,
|
||||
.y = (self.min_y + self.max_y) / 2.0,
|
||||
};
|
||||
}
|
||||
|
||||
/// Expands this box to include another box.
|
||||
pub fn expand(self: *BoundingBox2D, other: BoundingBox2D) void {
|
||||
self.min_x = @min(self.min_x, other.min_x);
|
||||
self.max_x = @max(self.max_x, other.max_x);
|
||||
self.min_y = @min(self.min_y, other.min_y);
|
||||
self.max_y = @max(self.max_y, other.max_y);
|
||||
}
|
||||
};
|
||||
|
||||
/// A 3D bounding box.
|
||||
pub const BoundingBox3D = struct {
|
||||
min_x: f64,
|
||||
max_x: f64,
|
||||
min_y: f64,
|
||||
max_y: f64,
|
||||
min_z: f64,
|
||||
max_z: f64,
|
||||
|
||||
/// Creates a 3D bounding box from center point and size.
|
||||
pub fn fromCenter(center_x: f64, center_y: f64, center_z: f64, width: f64, height: f64, depth: f64) BoundingBox3D {
|
||||
const half_w = width / 2.0;
|
||||
const half_h = height / 2.0;
|
||||
const half_d = depth / 2.0;
|
||||
return .{
|
||||
.min_x = center_x - half_w,
|
||||
.max_x = center_x + half_w,
|
||||
.min_y = center_y - half_h,
|
||||
.max_y = center_y + half_h,
|
||||
.min_z = center_z - half_d,
|
||||
.max_z = center_z + half_d,
|
||||
};
|
||||
}
|
||||
|
||||
/// Creates a bounding box from a point (zero-size box).
|
||||
pub fn fromPoint(x: f64, y: f64, z: f64) BoundingBox3D {
|
||||
return .{
|
||||
.min_x = x,
|
||||
.max_x = x,
|
||||
.min_y = y,
|
||||
.max_y = y,
|
||||
.min_z = z,
|
||||
.max_z = z,
|
||||
};
|
||||
}
|
||||
|
||||
/// Checks if this box intersects with another box.
|
||||
pub fn intersects(self: BoundingBox3D, other: BoundingBox3D) bool {
|
||||
return self.min_x <= other.max_x and
|
||||
self.max_x >= other.min_x and
|
||||
self.min_y <= other.max_y and
|
||||
self.max_y >= other.min_y and
|
||||
self.min_z <= other.max_z and
|
||||
self.max_z >= other.min_z;
|
||||
}
|
||||
|
||||
/// Returns the volume of the bounding box.
|
||||
pub fn volume(self: BoundingBox3D) f64 {
|
||||
return (self.max_x - self.min_x) * (self.max_y - self.min_y) * (self.max_z - self.min_z);
|
||||
}
|
||||
};
|
||||
|
||||
/// Geographic coordinates (latitude/longitude).
|
||||
pub const GeoCoord = struct {
|
||||
latitude: f64,
|
||||
longitude: f64,
|
||||
|
||||
/// Creates a bounding box around this point with a given radius in degrees.
|
||||
pub fn boundingBox(self: GeoCoord, radius_degrees: f64) BoundingBox2D {
|
||||
return .{
|
||||
.min_x = self.longitude - radius_degrees,
|
||||
.max_x = self.longitude + radius_degrees,
|
||||
.min_y = self.latitude - radius_degrees,
|
||||
.max_y = self.latitude + radius_degrees,
|
||||
};
|
||||
}
|
||||
|
||||
/// Approximate distance in kilometers to another coordinate (Haversine formula).
|
||||
pub fn distanceKm(self: GeoCoord, other: GeoCoord) f64 {
|
||||
const earth_radius_km = 6371.0;
|
||||
const lat1 = self.latitude * std.math.pi / 180.0;
|
||||
const lat2 = other.latitude * std.math.pi / 180.0;
|
||||
const delta_lat = (other.latitude - self.latitude) * std.math.pi / 180.0;
|
||||
const delta_lon = (other.longitude - self.longitude) * std.math.pi / 180.0;
|
||||
|
||||
const a = std.math.sin(delta_lat / 2.0) * std.math.sin(delta_lat / 2.0) +
|
||||
std.math.cos(lat1) * std.math.cos(lat2) *
|
||||
std.math.sin(delta_lon / 2.0) * std.math.sin(delta_lon / 2.0);
|
||||
const c = 2.0 * std.math.atan2(std.math.sqrt(a), std.math.sqrt(1.0 - a));
|
||||
|
||||
return earth_radius_km * c;
|
||||
}
|
||||
};
|
||||
|
||||
/// R-Tree helper functions for a database connection.
|
||||
pub const RTree = struct {
|
||||
db: *Database,
|
||||
allocator: std.mem.Allocator,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
/// Creates a new R-Tree helper bound to a database connection.
|
||||
pub fn init(db: *Database, allocator: std.mem.Allocator) Self {
|
||||
return .{ .db = db, .allocator = allocator };
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Table Management
|
||||
// ========================================================================
|
||||
|
||||
/// Creates a 2D R-Tree virtual table.
|
||||
///
|
||||
/// Example:
|
||||
/// ```zig
|
||||
/// var rtree = RTree.init(&db, allocator);
|
||||
/// try rtree.createTable2D("locations", "min_x", "max_x", "min_y", "max_y");
|
||||
/// ```
|
||||
pub fn createTable2D(
|
||||
self: *Self,
|
||||
table_name: []const u8,
|
||||
min_x_name: []const u8,
|
||||
max_x_name: []const u8,
|
||||
min_y_name: []const u8,
|
||||
max_y_name: []const u8,
|
||||
) !void {
|
||||
const sql = try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"CREATE VIRTUAL TABLE {s} USING rtree(id, {s}, {s}, {s}, {s})\x00",
|
||||
.{ table_name, min_x_name, max_x_name, min_y_name, max_y_name },
|
||||
);
|
||||
defer self.allocator.free(sql);
|
||||
try self.db.exec(sql[0 .. sql.len - 1 :0]);
|
||||
}
|
||||
|
||||
/// Creates a 3D R-Tree virtual table.
|
||||
pub fn createTable3D(
|
||||
self: *Self,
|
||||
table_name: []const u8,
|
||||
min_x_name: []const u8,
|
||||
max_x_name: []const u8,
|
||||
min_y_name: []const u8,
|
||||
max_y_name: []const u8,
|
||||
min_z_name: []const u8,
|
||||
max_z_name: []const u8,
|
||||
) !void {
|
||||
const sql = try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"CREATE VIRTUAL TABLE {s} USING rtree(id, {s}, {s}, {s}, {s}, {s}, {s})\x00",
|
||||
.{ table_name, min_x_name, max_x_name, min_y_name, max_y_name, min_z_name, max_z_name },
|
||||
);
|
||||
defer self.allocator.free(sql);
|
||||
try self.db.exec(sql[0 .. sql.len - 1 :0]);
|
||||
}
|
||||
|
||||
/// Creates a simple 2D R-Tree with default column names.
|
||||
pub fn createSimpleTable2D(self: *Self, table_name: []const u8) !void {
|
||||
try self.createTable2D(table_name, "min_x", "max_x", "min_y", "max_y");
|
||||
}
|
||||
|
||||
/// Creates a simple 3D R-Tree with default column names.
|
||||
pub fn createSimpleTable3D(self: *Self, table_name: []const u8) !void {
|
||||
try self.createTable3D(table_name, "min_x", "max_x", "min_y", "max_y", "min_z", "max_z");
|
||||
}
|
||||
|
||||
/// Creates a geographic R-Tree (using latitude/longitude).
|
||||
pub fn createGeoTable(self: *Self, table_name: []const u8) !void {
|
||||
try self.createTable2D(table_name, "min_lon", "max_lon", "min_lat", "max_lat");
|
||||
}
|
||||
|
||||
/// Drops an R-Tree table.
|
||||
pub fn dropTable(self: *Self, table_name: []const u8) !void {
|
||||
const sql = try std.fmt.allocPrint(self.allocator, "DROP TABLE IF EXISTS {s}\x00", .{table_name});
|
||||
defer self.allocator.free(sql);
|
||||
try self.db.exec(sql[0 .. sql.len - 1 :0]);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Insert Operations
|
||||
// ========================================================================
|
||||
|
||||
/// Inserts a 2D bounding box into an R-Tree.
|
||||
pub fn insert2D(self: *Self, table_name: []const u8, id: i64, box: BoundingBox2D) !void {
|
||||
const sql = try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"INSERT INTO {s} VALUES (?, ?, ?, ?, ?)\x00",
|
||||
.{table_name},
|
||||
);
|
||||
defer self.allocator.free(sql);
|
||||
|
||||
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindInt(1, id);
|
||||
try stmt.bindFloat(2, box.min_x);
|
||||
try stmt.bindFloat(3, box.max_x);
|
||||
try stmt.bindFloat(4, box.min_y);
|
||||
try stmt.bindFloat(5, box.max_y);
|
||||
|
||||
_ = try stmt.step();
|
||||
}
|
||||
|
||||
/// Inserts a 3D bounding box into an R-Tree.
|
||||
pub fn insert3D(self: *Self, table_name: []const u8, id: i64, box: BoundingBox3D) !void {
|
||||
const sql = try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"INSERT INTO {s} VALUES (?, ?, ?, ?, ?, ?, ?)\x00",
|
||||
.{table_name},
|
||||
);
|
||||
defer self.allocator.free(sql);
|
||||
|
||||
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindInt(1, id);
|
||||
try stmt.bindFloat(2, box.min_x);
|
||||
try stmt.bindFloat(3, box.max_x);
|
||||
try stmt.bindFloat(4, box.min_y);
|
||||
try stmt.bindFloat(5, box.max_y);
|
||||
try stmt.bindFloat(6, box.min_z);
|
||||
try stmt.bindFloat(7, box.max_z);
|
||||
|
||||
_ = try stmt.step();
|
||||
}
|
||||
|
||||
/// Inserts a point (as a zero-size bounding box) into a 2D R-Tree.
|
||||
pub fn insertPoint2D(self: *Self, table_name: []const u8, id: i64, x: f64, y: f64) !void {
|
||||
try self.insert2D(table_name, id, BoundingBox2D.fromPoint(x, y));
|
||||
}
|
||||
|
||||
/// Inserts a geographic coordinate into a geo R-Tree.
|
||||
pub fn insertGeo(self: *Self, table_name: []const u8, id: i64, coord: GeoCoord) !void {
|
||||
try self.insert2D(table_name, id, .{
|
||||
.min_x = coord.longitude,
|
||||
.max_x = coord.longitude,
|
||||
.min_y = coord.latitude,
|
||||
.max_y = coord.latitude,
|
||||
});
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Update Operations
|
||||
// ========================================================================
|
||||
|
||||
/// Updates a 2D bounding box in an R-Tree.
|
||||
pub fn update2D(self: *Self, table_name: []const u8, id: i64, box: BoundingBox2D) !void {
|
||||
const sql = try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"UPDATE {s} SET min_x=?, max_x=?, min_y=?, max_y=? WHERE id=?\x00",
|
||||
.{table_name},
|
||||
);
|
||||
defer self.allocator.free(sql);
|
||||
|
||||
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindFloat(1, box.min_x);
|
||||
try stmt.bindFloat(2, box.max_x);
|
||||
try stmt.bindFloat(3, box.min_y);
|
||||
try stmt.bindFloat(4, box.max_y);
|
||||
try stmt.bindInt(5, id);
|
||||
|
||||
_ = try stmt.step();
|
||||
}
|
||||
|
||||
/// Deletes an entry from an R-Tree.
|
||||
pub fn delete(self: *Self, table_name: []const u8, id: i64) !void {
|
||||
const sql = try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"DELETE FROM {s} WHERE id=?\x00",
|
||||
.{table_name},
|
||||
);
|
||||
defer self.allocator.free(sql);
|
||||
|
||||
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindInt(1, id);
|
||||
_ = try stmt.step();
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Query Operations
|
||||
// ========================================================================
|
||||
|
||||
/// Queries for all entries that intersect with a bounding box.
|
||||
/// Returns a prepared statement with results: id, min_x, max_x, min_y, max_y
|
||||
pub fn queryIntersects2D(self: *Self, table_name: []const u8, box: BoundingBox2D) !Statement {
|
||||
const sql = try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"SELECT * FROM {s} WHERE min_x <= ? AND max_x >= ? AND min_y <= ? AND max_y >= ?\x00",
|
||||
.{table_name},
|
||||
);
|
||||
defer self.allocator.free(sql);
|
||||
|
||||
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
|
||||
errdefer stmt.finalize();
|
||||
|
||||
try stmt.bindFloat(1, box.max_x);
|
||||
try stmt.bindFloat(2, box.min_x);
|
||||
try stmt.bindFloat(3, box.max_y);
|
||||
try stmt.bindFloat(4, box.min_y);
|
||||
|
||||
return stmt;
|
||||
}
|
||||
|
||||
/// Queries for all entries contained within a bounding box.
|
||||
pub fn queryContained2D(self: *Self, table_name: []const u8, box: BoundingBox2D) !Statement {
|
||||
const sql = try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"SELECT * FROM {s} WHERE min_x >= ? AND max_x <= ? AND min_y >= ? AND max_y <= ?\x00",
|
||||
.{table_name},
|
||||
);
|
||||
defer self.allocator.free(sql);
|
||||
|
||||
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
|
||||
errdefer stmt.finalize();
|
||||
|
||||
try stmt.bindFloat(1, box.min_x);
|
||||
try stmt.bindFloat(2, box.max_x);
|
||||
try stmt.bindFloat(3, box.min_y);
|
||||
try stmt.bindFloat(4, box.max_y);
|
||||
|
||||
return stmt;
|
||||
}
|
||||
|
||||
/// Queries for all entries near a point (within a given radius).
|
||||
pub fn queryNearPoint2D(self: *Self, table_name: []const u8, x: f64, y: f64, radius: f64) !Statement {
|
||||
const box = BoundingBox2D{
|
||||
.min_x = x - radius,
|
||||
.max_x = x + radius,
|
||||
.min_y = y - radius,
|
||||
.max_y = y + radius,
|
||||
};
|
||||
return self.queryIntersects2D(table_name, box);
|
||||
}
|
||||
|
||||
/// Queries for all entries near a geographic coordinate.
|
||||
/// radius is in degrees (approximately: 1 degree ≈ 111km at equator)
|
||||
pub fn queryNearGeo(self: *Self, table_name: []const u8, coord: GeoCoord, radius_degrees: f64) !Statement {
|
||||
return self.queryNearPoint2D(table_name, coord.longitude, coord.latitude, radius_degrees);
|
||||
}
|
||||
|
||||
/// Gets all entry IDs that intersect with a bounding box.
|
||||
pub fn getIntersectingIds2D(self: *Self, table_name: []const u8, box: BoundingBox2D) ![]i64 {
|
||||
var stmt = try self.queryIntersects2D(table_name, box);
|
||||
defer stmt.finalize();
|
||||
|
||||
var ids: std.ArrayListUnmanaged(i64) = .empty;
|
||||
errdefer ids.deinit(self.allocator);
|
||||
|
||||
while (try stmt.step()) {
|
||||
try ids.append(self.allocator, stmt.columnInt(0));
|
||||
}
|
||||
|
||||
return ids.toOwnedSlice(self.allocator);
|
||||
}
|
||||
|
||||
/// Frees an array of IDs returned by getIntersectingIds2D.
|
||||
pub fn freeIds(self: *Self, ids: []i64) void {
|
||||
self.allocator.free(ids);
|
||||
}
|
||||
|
||||
/// Counts entries that intersect with a bounding box.
|
||||
pub fn countIntersects2D(self: *Self, table_name: []const u8, box: BoundingBox2D) !i64 {
|
||||
const sql = try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"SELECT COUNT(*) FROM {s} WHERE min_x <= ? AND max_x >= ? AND min_y <= ? AND max_y >= ?\x00",
|
||||
.{table_name},
|
||||
);
|
||||
defer self.allocator.free(sql);
|
||||
|
||||
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindFloat(1, box.max_x);
|
||||
try stmt.bindFloat(2, box.min_x);
|
||||
try stmt.bindFloat(3, box.max_y);
|
||||
try stmt.bindFloat(4, box.min_y);
|
||||
|
||||
if (try stmt.step()) {
|
||||
return stmt.columnInt(0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Gets the total count of entries in an R-Tree.
|
||||
pub fn count(self: *Self, table_name: []const u8) !i64 {
|
||||
const sql = try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"SELECT COUNT(*) FROM {s}\x00",
|
||||
.{table_name},
|
||||
);
|
||||
defer self.allocator.free(sql);
|
||||
|
||||
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
|
||||
defer stmt.finalize();
|
||||
|
||||
if (try stmt.step()) {
|
||||
return stmt.columnInt(0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Gets the bounding box that encompasses all entries in the R-Tree.
|
||||
pub fn getBounds2D(self: *Self, table_name: []const u8) !?BoundingBox2D {
|
||||
const sql = try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"SELECT MIN(min_x), MAX(max_x), MIN(min_y), MAX(max_y) FROM {s}\x00",
|
||||
.{table_name},
|
||||
);
|
||||
defer self.allocator.free(sql);
|
||||
|
||||
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
|
||||
defer stmt.finalize();
|
||||
|
||||
if (try stmt.step()) {
|
||||
if (stmt.columnIsNull(0)) return null;
|
||||
return BoundingBox2D{
|
||||
.min_x = stmt.columnFloat(0),
|
||||
.max_x = stmt.columnFloat(1),
|
||||
.min_y = stmt.columnFloat(2),
|
||||
.max_y = stmt.columnFloat(3),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Join Operations (R-Tree with regular tables)
|
||||
// ========================================================================
|
||||
|
||||
/// Performs a spatial join between an R-Tree and a regular table.
|
||||
/// Returns entries from both tables where the R-Tree bbox intersects the query box.
|
||||
///
|
||||
/// Example:
|
||||
/// ```zig
|
||||
/// var stmt = try rtree.spatialJoin2D(
|
||||
/// "locations", // R-Tree table
|
||||
/// "places", // Regular table
|
||||
/// "id", // Join column in places
|
||||
/// &.{ "name", "type" }, // Columns to select from places
|
||||
/// query_box,
|
||||
/// );
|
||||
/// ```
|
||||
pub fn spatialJoin2D(
|
||||
self: *Self,
|
||||
rtree_table: []const u8,
|
||||
data_table: []const u8,
|
||||
join_column: []const u8,
|
||||
select_columns: []const []const u8,
|
||||
box: BoundingBox2D,
|
||||
) !Statement {
|
||||
// Build select columns
|
||||
var col_buf: [2048]u8 = undefined;
|
||||
var col_len: usize = 0;
|
||||
|
||||
@memcpy(col_buf[col_len..][0..4], "r.id");
|
||||
col_len += 4;
|
||||
|
||||
for (select_columns) |col| {
|
||||
@memcpy(col_buf[col_len..][0..4], ", d.");
|
||||
col_len += 4;
|
||||
@memcpy(col_buf[col_len..][0..col.len], col);
|
||||
col_len += col.len;
|
||||
}
|
||||
|
||||
const sql = try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"SELECT {s} FROM {s} r JOIN {s} d ON r.id = d.{s} WHERE r.min_x <= ? AND r.max_x >= ? AND r.min_y <= ? AND r.max_y >= ?\x00",
|
||||
.{ col_buf[0..col_len], rtree_table, data_table, join_column },
|
||||
);
|
||||
defer self.allocator.free(sql);
|
||||
|
||||
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
|
||||
errdefer stmt.finalize();
|
||||
|
||||
try stmt.bindFloat(1, box.max_x);
|
||||
try stmt.bindFloat(2, box.min_x);
|
||||
try stmt.bindFloat(3, box.max_y);
|
||||
try stmt.bindFloat(4, box.min_y);
|
||||
|
||||
return stmt;
|
||||
}
|
||||
};
|
||||
|
|
@ -1,691 +0,0 @@
|
|||
//! SQLite prepared statement handling
|
||||
//!
|
||||
//! Provides the Statement struct for executing SQL queries with parameter binding.
|
||||
|
||||
const std = @import("std");
|
||||
const c = @import("c.zig").c;
|
||||
const errors = @import("errors.zig");
|
||||
const types = @import("types.zig");
|
||||
|
||||
const Error = errors.Error;
|
||||
const resultToError = errors.resultToError;
|
||||
const ColumnType = types.ColumnType;
|
||||
|
||||
// Forward declaration - Database will be imported where needed
|
||||
pub const Database = @import("database.zig").Database;
|
||||
|
||||
/// Prepared SQL statement
|
||||
pub const Statement = struct {
|
||||
handle: ?*c.sqlite3_stmt,
|
||||
db: *Database,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
/// Finalizes (destroys) the statement.
|
||||
pub fn finalize(self: *Self) void {
|
||||
if (self.handle) |h| {
|
||||
_ = c.sqlite3_finalize(h);
|
||||
self.handle = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets the statement for re-execution.
|
||||
pub fn reset(self: *Self) Error!void {
|
||||
const result = c.sqlite3_reset(self.handle);
|
||||
if (result != c.SQLITE_OK) {
|
||||
return resultToError(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// Clears all parameter bindings.
|
||||
pub fn clearBindings(self: *Self) Error!void {
|
||||
const result = c.sqlite3_clear_bindings(self.handle);
|
||||
if (result != c.SQLITE_OK) {
|
||||
return resultToError(result);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Statement metadata
|
||||
// ========================================================================
|
||||
|
||||
/// Returns the SQL text of the statement.
|
||||
pub fn sql(self: *Self) ?[]const u8 {
|
||||
const s = c.sqlite3_sql(self.handle);
|
||||
if (s) |ptr| {
|
||||
return std.mem.span(ptr);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns whether the statement is read-only.
|
||||
pub fn isReadOnly(self: *Self) bool {
|
||||
return c.sqlite3_stmt_readonly(self.handle) != 0;
|
||||
}
|
||||
|
||||
/// Returns the number of parameters in the statement.
|
||||
pub fn parameterCount(self: *Self) u32 {
|
||||
return @intCast(c.sqlite3_bind_parameter_count(self.handle));
|
||||
}
|
||||
|
||||
/// Returns the index of a named parameter.
|
||||
/// Supports :name, @name, and $name styles.
|
||||
/// Returns null if the parameter is not found.
|
||||
pub fn parameterIndex(self: *Self, name: [:0]const u8) ?u32 {
|
||||
const idx = c.sqlite3_bind_parameter_index(self.handle, name.ptr);
|
||||
if (idx == 0) return null;
|
||||
return @intCast(idx);
|
||||
}
|
||||
|
||||
/// Returns the name of a parameter by index.
|
||||
/// Returns null if the parameter has no name (positional ?).
|
||||
pub fn parameterName(self: *Self, index: u32) ?[]const u8 {
|
||||
const name = c.sqlite3_bind_parameter_name(self.handle, @intCast(index));
|
||||
if (name) |n| {
|
||||
return std.mem.span(n);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Bind parameters (1-indexed as per SQLite convention)
|
||||
// ========================================================================
|
||||
|
||||
/// Binds NULL to a parameter.
|
||||
pub fn bindNull(self: *Self, index: u32) Error!void {
|
||||
const result = c.sqlite3_bind_null(self.handle, @intCast(index));
|
||||
if (result != c.SQLITE_OK) {
|
||||
return resultToError(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// Binds an integer to a parameter.
|
||||
pub fn bindInt(self: *Self, index: u32, value: i64) Error!void {
|
||||
const result = c.sqlite3_bind_int64(self.handle, @intCast(index), value);
|
||||
if (result != c.SQLITE_OK) {
|
||||
return resultToError(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// Binds a float to a parameter.
|
||||
pub fn bindFloat(self: *Self, index: u32, value: f64) Error!void {
|
||||
const result = c.sqlite3_bind_double(self.handle, @intCast(index), value);
|
||||
if (result != c.SQLITE_OK) {
|
||||
return resultToError(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// Binds text to a parameter. The text is copied by SQLite (SQLITE_TRANSIENT).
|
||||
pub fn bindText(self: *Self, index: u32, value: []const u8) Error!void {
|
||||
const result = c.sqlite3_bind_text(
|
||||
self.handle,
|
||||
@intCast(index),
|
||||
value.ptr,
|
||||
@intCast(value.len),
|
||||
c.SQLITE_TRANSIENT,
|
||||
);
|
||||
if (result != c.SQLITE_OK) {
|
||||
return resultToError(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// Binds a blob to a parameter. The blob is copied by SQLite (SQLITE_TRANSIENT).
|
||||
pub fn bindBlob(self: *Self, index: u32, value: []const u8) Error!void {
|
||||
const result = c.sqlite3_bind_blob(
|
||||
self.handle,
|
||||
@intCast(index),
|
||||
value.ptr,
|
||||
@intCast(value.len),
|
||||
c.SQLITE_TRANSIENT,
|
||||
);
|
||||
if (result != c.SQLITE_OK) {
|
||||
return resultToError(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// Binds a boolean to a parameter (as integer 0 or 1).
|
||||
pub fn bindBool(self: *Self, index: u32, value: bool) Error!void {
|
||||
try self.bindInt(index, if (value) 1 else 0);
|
||||
}
|
||||
|
||||
/// Binds a zeroblob (a blob of zeros) to a parameter.
|
||||
pub fn bindZeroblob(self: *Self, index: u32, size: u32) Error!void {
|
||||
const result = c.sqlite3_bind_zeroblob(self.handle, @intCast(index), @intCast(size));
|
||||
if (result != c.SQLITE_OK) {
|
||||
return resultToError(result);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Named parameter binding
|
||||
// ========================================================================
|
||||
|
||||
/// Binds NULL to a named parameter.
|
||||
pub fn bindNullNamed(self: *Self, name: [:0]const u8) Error!void {
|
||||
const idx = self.parameterIndex(name) orelse return Error.Range;
|
||||
try self.bindNull(idx);
|
||||
}
|
||||
|
||||
/// Binds an integer to a named parameter.
|
||||
pub fn bindIntNamed(self: *Self, name: [:0]const u8, value: i64) Error!void {
|
||||
const idx = self.parameterIndex(name) orelse return Error.Range;
|
||||
try self.bindInt(idx, value);
|
||||
}
|
||||
|
||||
/// Binds a float to a named parameter.
|
||||
pub fn bindFloatNamed(self: *Self, name: [:0]const u8, value: f64) Error!void {
|
||||
const idx = self.parameterIndex(name) orelse return Error.Range;
|
||||
try self.bindFloat(idx, value);
|
||||
}
|
||||
|
||||
/// Binds text to a named parameter.
|
||||
pub fn bindTextNamed(self: *Self, name: [:0]const u8, value: []const u8) Error!void {
|
||||
const idx = self.parameterIndex(name) orelse return Error.Range;
|
||||
try self.bindText(idx, value);
|
||||
}
|
||||
|
||||
/// Binds a blob to a named parameter.
|
||||
pub fn bindBlobNamed(self: *Self, name: [:0]const u8, value: []const u8) Error!void {
|
||||
const idx = self.parameterIndex(name) orelse return Error.Range;
|
||||
try self.bindBlob(idx, value);
|
||||
}
|
||||
|
||||
/// Binds a boolean to a named parameter.
|
||||
pub fn bindBoolNamed(self: *Self, name: [:0]const u8, value: bool) Error!void {
|
||||
const idx = self.parameterIndex(name) orelse return Error.Range;
|
||||
try self.bindBool(idx, value);
|
||||
}
|
||||
|
||||
/// Binds a timestamp as ISO8601 text (YYYY-MM-DD HH:MM:SS).
|
||||
pub fn bindTimestamp(self: *Self, index: u32, ts: i64) Error!void {
|
||||
const epoch_seconds = std.time.epoch.EpochSeconds{ .secs = @intCast(ts) };
|
||||
const day_seconds = epoch_seconds.getDaySeconds();
|
||||
const year_day = epoch_seconds.getEpochDay().calculateYearDay();
|
||||
const month_day = year_day.calculateMonthDay();
|
||||
|
||||
var buf: [20]u8 = undefined;
|
||||
const formatted = std.fmt.bufPrint(&buf, "{d:0>4}-{d:0>2}-{d:0>2} {d:0>2}:{d:0>2}:{d:0>2}", .{
|
||||
year_day.year,
|
||||
@intFromEnum(month_day.month),
|
||||
@as(u8, month_day.day_index) + 1,
|
||||
day_seconds.getHoursIntoDay(),
|
||||
day_seconds.getMinutesIntoHour(),
|
||||
day_seconds.getSecondsIntoMinute(),
|
||||
}) catch return Error.SqliteError;
|
||||
|
||||
try self.bindText(index, formatted);
|
||||
}
|
||||
|
||||
/// Binds a timestamp to a named parameter as ISO8601 text.
|
||||
pub fn bindTimestampNamed(self: *Self, name: [:0]const u8, ts: i64) Error!void {
|
||||
const idx = self.parameterIndex(name) orelse return Error.Range;
|
||||
try self.bindTimestamp(idx, ts);
|
||||
}
|
||||
|
||||
/// Binds the current time as ISO8601 text.
|
||||
pub fn bindCurrentTime(self: *Self, index: u32) Error!void {
|
||||
const now = std.time.timestamp();
|
||||
try self.bindTimestamp(index, now);
|
||||
}
|
||||
|
||||
/// Binds the current time to a named parameter.
|
||||
pub fn bindCurrentTimeNamed(self: *Self, name: [:0]const u8) Error!void {
|
||||
const idx = self.parameterIndex(name) orelse return Error.Range;
|
||||
try self.bindCurrentTime(idx);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Execution
|
||||
// ========================================================================
|
||||
|
||||
/// Executes one step of the statement.
|
||||
/// Returns true if there's a row available (for SELECT statements).
|
||||
/// Returns false when done (SQLITE_DONE).
|
||||
pub fn step(self: *Self) Error!bool {
|
||||
const result = c.sqlite3_step(self.handle);
|
||||
return switch (result) {
|
||||
c.SQLITE_ROW => true,
|
||||
c.SQLITE_DONE => false,
|
||||
else => resultToError(result),
|
||||
};
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Column access (0-indexed as per SQLite convention)
|
||||
// ========================================================================
|
||||
|
||||
/// Returns the number of columns in the result set.
|
||||
pub fn columnCount(self: *Self) u32 {
|
||||
return @intCast(c.sqlite3_column_count(self.handle));
|
||||
}
|
||||
|
||||
/// Returns the name of a column.
|
||||
pub fn columnName(self: *Self, index: u32) ?[]const u8 {
|
||||
const name = c.sqlite3_column_name(self.handle, @intCast(index));
|
||||
if (name) |n| {
|
||||
return std.mem.span(n);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns the type of a column value.
|
||||
pub fn columnType(self: *Self, index: u32) ColumnType {
|
||||
const col_type = c.sqlite3_column_type(self.handle, @intCast(index));
|
||||
return switch (col_type) {
|
||||
c.SQLITE_INTEGER => .integer,
|
||||
c.SQLITE_FLOAT => .float,
|
||||
c.SQLITE_TEXT => .text,
|
||||
c.SQLITE_BLOB => .blob,
|
||||
c.SQLITE_NULL => .null_value,
|
||||
else => .null_value,
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns an integer column value.
|
||||
pub fn columnInt(self: *Self, index: u32) i64 {
|
||||
return c.sqlite3_column_int64(self.handle, @intCast(index));
|
||||
}
|
||||
|
||||
/// Returns a float column value.
|
||||
pub fn columnFloat(self: *Self, index: u32) f64 {
|
||||
return c.sqlite3_column_double(self.handle, @intCast(index));
|
||||
}
|
||||
|
||||
/// Returns a text column value.
|
||||
pub fn columnText(self: *Self, index: u32) ?[]const u8 {
|
||||
const len = c.sqlite3_column_bytes(self.handle, @intCast(index));
|
||||
const text = c.sqlite3_column_text(self.handle, @intCast(index));
|
||||
if (text) |t| {
|
||||
return t[0..@intCast(len)];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns a blob column value.
|
||||
pub fn columnBlob(self: *Self, index: u32) ?[]const u8 {
|
||||
const len = c.sqlite3_column_bytes(self.handle, @intCast(index));
|
||||
const blob = c.sqlite3_column_blob(self.handle, @intCast(index));
|
||||
if (blob) |b| {
|
||||
const ptr: [*]const u8 = @ptrCast(b);
|
||||
return ptr[0..@intCast(len)];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns true if the column is NULL.
|
||||
pub fn columnIsNull(self: *Self, index: u32) bool {
|
||||
return self.columnType(index) == .null_value;
|
||||
}
|
||||
|
||||
/// Returns a boolean column value (interprets 0 as false, non-zero as true).
|
||||
pub fn columnBool(self: *Self, index: u32) bool {
|
||||
return self.columnInt(index) != 0;
|
||||
}
|
||||
|
||||
/// Returns the size in bytes of a blob or text column.
|
||||
pub fn columnBytes(self: *Self, index: u32) u32 {
|
||||
return @intCast(c.sqlite3_column_bytes(self.handle, @intCast(index)));
|
||||
}
|
||||
|
||||
/// Returns the declared type of a column.
|
||||
pub fn columnDeclType(self: *Self, index: u32) ?[]const u8 {
|
||||
const dtype = c.sqlite3_column_decltype(self.handle, @intCast(index));
|
||||
if (dtype) |d| {
|
||||
return std.mem.span(d);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns the database name for a column result.
|
||||
pub fn columnDatabaseName(self: *Self, index: u32) ?[]const u8 {
|
||||
const name = c.sqlite3_column_database_name(self.handle, @intCast(index));
|
||||
if (name) |n| {
|
||||
return std.mem.span(n);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns the table name for a column result.
|
||||
pub fn columnTableName(self: *Self, index: u32) ?[]const u8 {
|
||||
const name = c.sqlite3_column_table_name(self.handle, @intCast(index));
|
||||
if (name) |n| {
|
||||
return std.mem.span(n);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns the origin column name for a column result.
|
||||
pub fn columnOriginName(self: *Self, index: u32) ?[]const u8 {
|
||||
const name = c.sqlite3_column_origin_name(self.handle, @intCast(index));
|
||||
if (name) |n| {
|
||||
return std.mem.span(n);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns the SQL text with bound parameters expanded.
|
||||
pub fn expandedSql(self: *Self, allocator: std.mem.Allocator) ?[]u8 {
|
||||
const expanded = c.sqlite3_expanded_sql(self.handle);
|
||||
if (expanded == null) return null;
|
||||
|
||||
const len = std.mem.len(expanded);
|
||||
const result = allocator.alloc(u8, len) catch return null;
|
||||
@memcpy(result, expanded[0..len]);
|
||||
|
||||
c.sqlite3_free(expanded);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Batch binding with tuples/structs
|
||||
// ========================================================================
|
||||
|
||||
/// Binds all values from a tuple or struct to positional parameters.
|
||||
/// Parameters are bound starting at index 1.
|
||||
///
|
||||
/// Example:
|
||||
/// ```zig
|
||||
/// var stmt = try db.prepare("INSERT INTO users (name, age, score) VALUES (?, ?, ?)");
|
||||
/// try stmt.bindAll(.{ "Alice", @as(i64, 30), @as(f64, 95.5) });
|
||||
/// ```
|
||||
///
|
||||
/// Supported types:
|
||||
/// - `[]const u8`, `[:0]const u8` -> bindText
|
||||
/// - `i64`, `i32`, `i16`, `i8`, `u32`, `u16`, `u8` -> bindInt
|
||||
/// - `f64`, `f32` -> bindFloat
|
||||
/// - `bool` -> bindBool
|
||||
/// - `@TypeOf(null)` -> bindNull
|
||||
/// - `?T` (optionals) -> bindNull if null, otherwise bind inner value
|
||||
pub fn bindAll(self: *Self, values: anytype) Error!void {
|
||||
const T = @TypeOf(values);
|
||||
const info = @typeInfo(T);
|
||||
|
||||
if (info == .@"struct") {
|
||||
const fields = info.@"struct".fields;
|
||||
inline for (fields, 0..) |field, i| {
|
||||
const value = @field(values, field.name);
|
||||
try self.bindValue(@intCast(i + 1), value);
|
||||
}
|
||||
} else {
|
||||
@compileError("bindAll expects a tuple or struct, got " ++ @typeName(T));
|
||||
}
|
||||
}
|
||||
|
||||
/// Binds a single value of any supported type to a parameter index.
|
||||
pub fn bindValue(self: *Self, index: u32, value: anytype) Error!void {
|
||||
const T = @TypeOf(value);
|
||||
const info = @typeInfo(T);
|
||||
|
||||
// Handle optionals
|
||||
if (info == .optional) {
|
||||
if (value) |v| {
|
||||
return self.bindValue(index, v);
|
||||
} else {
|
||||
return self.bindNull(index);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle null type
|
||||
if (T == @TypeOf(null)) {
|
||||
return self.bindNull(index);
|
||||
}
|
||||
|
||||
// Handle pointers to arrays (string literals)
|
||||
if (info == .pointer) {
|
||||
const child = info.pointer.child;
|
||||
if (info.pointer.size == .slice) {
|
||||
// []const u8 or []u8
|
||||
if (child == u8) {
|
||||
return self.bindText(index, value);
|
||||
}
|
||||
} else if (info.pointer.size == .one) {
|
||||
// *const [N]u8 (string literal)
|
||||
const child_info = @typeInfo(child);
|
||||
if (child_info == .array and child_info.array.child == u8) {
|
||||
return self.bindText(index, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle arrays
|
||||
if (info == .array and info.array.child == u8) {
|
||||
return self.bindText(index, &value);
|
||||
}
|
||||
|
||||
// Handle integers
|
||||
if (info == .int or info == .comptime_int) {
|
||||
return self.bindInt(index, @intCast(value));
|
||||
}
|
||||
|
||||
// Handle floats
|
||||
if (info == .float or info == .comptime_float) {
|
||||
return self.bindFloat(index, @floatCast(value));
|
||||
}
|
||||
|
||||
// Handle booleans
|
||||
if (T == bool) {
|
||||
return self.bindBool(index, value);
|
||||
}
|
||||
|
||||
@compileError("Unsupported type for bindValue: " ++ @typeName(T));
|
||||
}
|
||||
|
||||
/// Resets the statement and binds new values in one call.
|
||||
/// Useful for executing the same statement multiple times with different values.
|
||||
///
|
||||
/// Example:
|
||||
/// ```zig
|
||||
/// var stmt = try db.prepare("INSERT INTO users (name, age) VALUES (?, ?)");
|
||||
/// try stmt.rebind(.{ "Alice", @as(i64, 30) });
|
||||
/// _ = try stmt.step();
|
||||
/// try stmt.rebind(.{ "Bob", @as(i64, 25) });
|
||||
/// _ = try stmt.step();
|
||||
/// ```
|
||||
pub fn rebind(self: *Self, values: anytype) Error!void {
|
||||
try self.reset();
|
||||
try self.clearBindings();
|
||||
try self.bindAll(values);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Row Iterator
|
||||
// ========================================================================
|
||||
|
||||
/// Returns an iterator over result rows.
|
||||
/// This provides a more idiomatic Zig interface for iterating over query results.
|
||||
///
|
||||
/// Example:
|
||||
/// ```zig
|
||||
/// var stmt = try db.prepare("SELECT id, name FROM users");
|
||||
/// defer stmt.finalize();
|
||||
///
|
||||
/// var iter = stmt.iterator();
|
||||
/// while (try iter.next()) |row| {
|
||||
/// const id = row.int(0);
|
||||
/// const name = row.text(1) orelse "(null)";
|
||||
/// std.debug.print("User {}: {s}\n", .{ id, name });
|
||||
/// }
|
||||
/// ```
|
||||
pub fn iterator(self: *Self) RowIterator {
|
||||
return RowIterator{ .stmt = self };
|
||||
}
|
||||
|
||||
/// Convenience method to iterate with a callback.
|
||||
/// The callback receives a Row for each result row.
|
||||
///
|
||||
/// Example:
|
||||
/// ```zig
|
||||
/// try stmt.forEach(struct {
|
||||
/// fn call(row: Row) void {
|
||||
/// std.debug.print("{}: {s}\n", .{ row.int(0), row.text(1) orelse "" });
|
||||
/// }
|
||||
/// }.call);
|
||||
/// ```
|
||||
pub fn forEach(self: *Self, callback: *const fn (Row) void) Error!void {
|
||||
while (try self.step()) {
|
||||
callback(Row{ .stmt = self });
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes the statement and collects all rows into a slice.
|
||||
/// Each row is represented as an array of column values.
|
||||
/// Caller owns the returned memory.
|
||||
pub fn collectAll(self: *Self, allocator: std.mem.Allocator) ![]Row.Values {
|
||||
var rows = std.ArrayList(Row.Values).init(allocator);
|
||||
errdefer {
|
||||
for (rows.items) |*r| r.deinit(allocator);
|
||||
rows.deinit();
|
||||
}
|
||||
|
||||
while (try self.step()) {
|
||||
const row = Row{ .stmt = self };
|
||||
try rows.append(try row.toValues(allocator));
|
||||
}
|
||||
|
||||
return rows.toOwnedSlice();
|
||||
}
|
||||
};
|
||||
|
||||
/// Row iterator for idiomatic iteration over query results.
|
||||
pub const RowIterator = struct {
|
||||
stmt: *Statement,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
/// Advances to the next row.
|
||||
/// Returns a Row if there's data available, null if iteration is complete.
|
||||
pub fn next(self: *Self) Error!?Row {
|
||||
if (try self.stmt.step()) {
|
||||
return Row{ .stmt = self.stmt };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Resets the iterator to the beginning.
|
||||
pub fn reset(self: *Self) Error!void {
|
||||
try self.stmt.reset();
|
||||
}
|
||||
};
|
||||
|
||||
/// Represents a single row in a query result.
|
||||
/// Provides convenient access to column values.
|
||||
pub const Row = struct {
|
||||
stmt: *Statement,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
/// Returns the number of columns.
|
||||
pub fn columnCount(self: Self) u32 {
|
||||
return self.stmt.columnCount();
|
||||
}
|
||||
|
||||
/// Returns an integer column value.
|
||||
pub fn int(self: Self, index: u32) i64 {
|
||||
return self.stmt.columnInt(index);
|
||||
}
|
||||
|
||||
/// Returns a float column value.
|
||||
pub fn float(self: Self, index: u32) f64 {
|
||||
return self.stmt.columnFloat(index);
|
||||
}
|
||||
|
||||
/// Returns a text column value.
|
||||
pub fn text(self: Self, index: u32) ?[]const u8 {
|
||||
return self.stmt.columnText(index);
|
||||
}
|
||||
|
||||
/// Returns a blob column value.
|
||||
pub fn blob(self: Self, index: u32) ?[]const u8 {
|
||||
return self.stmt.columnBlob(index);
|
||||
}
|
||||
|
||||
/// Returns a boolean column value.
|
||||
pub fn boolean(self: Self, index: u32) bool {
|
||||
return self.stmt.columnBool(index);
|
||||
}
|
||||
|
||||
/// Returns true if the column is NULL.
|
||||
pub fn isNull(self: Self, index: u32) bool {
|
||||
return self.stmt.columnIsNull(index);
|
||||
}
|
||||
|
||||
/// Returns the column type.
|
||||
pub fn columnType(self: Self, index: u32) ColumnType {
|
||||
return self.stmt.columnType(index);
|
||||
}
|
||||
|
||||
/// Returns the column name.
|
||||
pub fn columnName(self: Self, index: u32) ?[]const u8 {
|
||||
return self.stmt.columnName(index);
|
||||
}
|
||||
|
||||
/// A value that can hold any SQLite column type.
|
||||
pub const Value = union(ColumnType) {
|
||||
integer: i64,
|
||||
float: f64,
|
||||
text: []const u8,
|
||||
blob: []const u8,
|
||||
null_value: void,
|
||||
};
|
||||
|
||||
/// Owned values for a complete row.
|
||||
pub const Values = struct {
|
||||
items: []Value,
|
||||
text_copies: [][]u8,
|
||||
allocator: std.mem.Allocator,
|
||||
|
||||
pub fn deinit(self: *Values, allocator: std.mem.Allocator) void {
|
||||
for (self.text_copies) |copy| {
|
||||
allocator.free(copy);
|
||||
}
|
||||
allocator.free(self.text_copies);
|
||||
allocator.free(self.items);
|
||||
}
|
||||
};
|
||||
|
||||
/// Converts the current row to owned Values.
|
||||
/// Text and blob values are copied so they persist after iteration.
|
||||
pub fn toValues(self: Self, allocator: std.mem.Allocator) !Values {
|
||||
const count = self.columnCount();
|
||||
const items = try allocator.alloc(Value, count);
|
||||
errdefer allocator.free(items);
|
||||
|
||||
var text_copies = std.ArrayList([]u8).init(allocator);
|
||||
errdefer {
|
||||
for (text_copies.items) |copy| allocator.free(copy);
|
||||
text_copies.deinit();
|
||||
}
|
||||
|
||||
for (0..count) |i| {
|
||||
const idx: u32 = @intCast(i);
|
||||
const col_type = self.columnType(idx);
|
||||
items[i] = switch (col_type) {
|
||||
.integer => .{ .integer = self.int(idx) },
|
||||
.float => .{ .float = self.float(idx) },
|
||||
.text => blk: {
|
||||
if (self.text(idx)) |t| {
|
||||
const copy = try allocator.dupe(u8, t);
|
||||
try text_copies.append(copy);
|
||||
break :blk .{ .text = copy };
|
||||
}
|
||||
break :blk .{ .null_value = {} };
|
||||
},
|
||||
.blob => blk: {
|
||||
if (self.blob(idx)) |b| {
|
||||
const copy = try allocator.dupe(u8, b);
|
||||
try text_copies.append(copy);
|
||||
break :blk .{ .blob = copy };
|
||||
}
|
||||
break :blk .{ .null_value = {} };
|
||||
},
|
||||
.null_value => .{ .null_value = {} },
|
||||
};
|
||||
}
|
||||
|
||||
return Values{
|
||||
.items = items,
|
||||
.text_copies = try text_copies.toOwnedSlice(),
|
||||
.allocator = allocator,
|
||||
};
|
||||
}
|
||||
};
|
||||
154
src/types.zig
154
src/types.zig
|
|
@ -1,154 +0,0 @@
|
|||
//! Common types for zsqlite
|
||||
//!
|
||||
//! Contains enums, flags, and type definitions shared across modules.
|
||||
|
||||
const c = @import("c.zig").c;
|
||||
|
||||
/// Flags for opening a database connection
|
||||
pub const OpenFlags = struct {
|
||||
read_only: bool = false,
|
||||
read_write: bool = true,
|
||||
create: bool = true,
|
||||
uri: bool = false,
|
||||
memory: bool = false,
|
||||
no_mutex: bool = false,
|
||||
full_mutex: bool = false,
|
||||
|
||||
/// Converts flags to SQLite integer format
|
||||
pub fn toInt(self: OpenFlags) c_int {
|
||||
var flags: c_int = 0;
|
||||
|
||||
if (self.read_only) {
|
||||
flags |= c.SQLITE_OPEN_READONLY;
|
||||
} else if (self.read_write) {
|
||||
flags |= c.SQLITE_OPEN_READWRITE;
|
||||
if (self.create) {
|
||||
flags |= c.SQLITE_OPEN_CREATE;
|
||||
}
|
||||
}
|
||||
|
||||
if (self.uri) flags |= c.SQLITE_OPEN_URI;
|
||||
if (self.memory) flags |= c.SQLITE_OPEN_MEMORY;
|
||||
if (self.no_mutex) flags |= c.SQLITE_OPEN_NOMUTEX;
|
||||
if (self.full_mutex) flags |= c.SQLITE_OPEN_FULLMUTEX;
|
||||
|
||||
return flags;
|
||||
}
|
||||
};
|
||||
|
||||
/// SQLite column types
|
||||
pub const ColumnType = enum(c_int) {
|
||||
integer = 1,
|
||||
float = 2,
|
||||
text = 3,
|
||||
blob = 4,
|
||||
null_value = 5,
|
||||
|
||||
pub fn fromInt(value: c_int) ColumnType {
|
||||
return switch (value) {
|
||||
1 => .integer,
|
||||
2 => .float,
|
||||
3 => .text,
|
||||
4 => .blob,
|
||||
else => .null_value,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// SQLite limit types for getLimit/setLimit
|
||||
pub const Limit = enum(c_int) {
|
||||
/// Maximum length of a string or BLOB
|
||||
length = 0,
|
||||
/// Maximum length of a SQL statement
|
||||
sql_length = 1,
|
||||
/// Maximum number of columns
|
||||
column = 2,
|
||||
/// Maximum depth of expression tree
|
||||
expr_depth = 3,
|
||||
/// Maximum number of terms in compound SELECT
|
||||
compound_select = 4,
|
||||
/// Maximum number of VDBE operations
|
||||
vdbe_op = 5,
|
||||
/// Maximum number of function arguments
|
||||
function_arg = 6,
|
||||
/// Maximum number of attached databases
|
||||
attached = 7,
|
||||
/// Maximum length of LIKE/GLOB pattern
|
||||
like_pattern_length = 8,
|
||||
/// Maximum number of bound parameters
|
||||
variable_number = 9,
|
||||
/// Maximum depth of trigger recursion
|
||||
trigger_depth = 10,
|
||||
/// Maximum number of worker threads
|
||||
worker_threads = 11,
|
||||
};
|
||||
|
||||
/// Operation type for update hooks
|
||||
pub const UpdateOperation = enum(c_int) {
|
||||
insert = 18, // SQLITE_INSERT
|
||||
update = 23, // SQLITE_UPDATE
|
||||
delete = 9, // SQLITE_DELETE
|
||||
|
||||
pub fn fromInt(value: c_int) ?UpdateOperation {
|
||||
return switch (value) {
|
||||
18 => .insert,
|
||||
23 => .update,
|
||||
9 => .delete,
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// Authorization action codes
|
||||
pub const AuthAction = enum(c_int) {
|
||||
create_index = 1,
|
||||
create_table = 2,
|
||||
create_temp_index = 3,
|
||||
create_temp_table = 4,
|
||||
create_temp_trigger = 5,
|
||||
create_temp_view = 6,
|
||||
create_trigger = 7,
|
||||
create_view = 8,
|
||||
delete = 9,
|
||||
drop_index = 10,
|
||||
drop_table = 11,
|
||||
drop_temp_index = 12,
|
||||
drop_temp_table = 13,
|
||||
drop_temp_trigger = 14,
|
||||
drop_temp_view = 15,
|
||||
drop_trigger = 16,
|
||||
drop_view = 17,
|
||||
insert = 18,
|
||||
pragma = 19,
|
||||
read = 20,
|
||||
select = 21,
|
||||
transaction = 22,
|
||||
update = 23,
|
||||
attach = 24,
|
||||
detach = 25,
|
||||
alter_table = 26,
|
||||
reindex = 27,
|
||||
analyze = 28,
|
||||
create_vtable = 29,
|
||||
drop_vtable = 30,
|
||||
function = 31,
|
||||
savepoint = 32,
|
||||
recursive = 33,
|
||||
|
||||
pub fn fromInt(value: c_int) ?AuthAction {
|
||||
if (value >= 1 and value <= 33) {
|
||||
return @enumFromInt(value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/// Authorization result codes
|
||||
pub const AuthResult = enum(c_int) {
|
||||
/// Allow the action
|
||||
ok = 0,
|
||||
/// Deny the action with an error
|
||||
deny = 1,
|
||||
/// Silently disallow (return NULL or skip)
|
||||
ignore = 2,
|
||||
};
|
||||
321
src/vtable.zig
321
src/vtable.zig
|
|
@ -1,321 +0,0 @@
|
|||
//! Virtual Table API
|
||||
//!
|
||||
//! Provides the foundation for creating custom SQLite virtual tables in Zig.
|
||||
//! Virtual tables allow you to expose any data source (files, APIs, computed data)
|
||||
//! as if it were a regular SQL table.
|
||||
//!
|
||||
//! This is an advanced feature - most users should use the higher-level helpers
|
||||
//! like FTS5, JSON, or R-Tree modules instead.
|
||||
|
||||
const std = @import("std");
|
||||
const c = @import("c.zig").c;
|
||||
const Database = @import("database.zig").Database;
|
||||
const errors = @import("errors.zig");
|
||||
|
||||
const Error = errors.Error;
|
||||
const resultToError = errors.resultToError;
|
||||
|
||||
/// Index constraint operators.
|
||||
pub const ConstraintOp = enum(u8) {
|
||||
eq = 2, // SQLITE_INDEX_CONSTRAINT_EQ
|
||||
gt = 4, // SQLITE_INDEX_CONSTRAINT_GT
|
||||
le = 8, // SQLITE_INDEX_CONSTRAINT_LE
|
||||
lt = 16, // SQLITE_INDEX_CONSTRAINT_LT
|
||||
ge = 32, // SQLITE_INDEX_CONSTRAINT_GE
|
||||
match = 64, // SQLITE_INDEX_CONSTRAINT_MATCH
|
||||
like = 65, // SQLITE_INDEX_CONSTRAINT_LIKE
|
||||
glob = 66, // SQLITE_INDEX_CONSTRAINT_GLOB
|
||||
regexp = 67, // SQLITE_INDEX_CONSTRAINT_REGEXP
|
||||
ne = 68, // SQLITE_INDEX_CONSTRAINT_NE
|
||||
isnot = 69, // SQLITE_INDEX_CONSTRAINT_ISNOT
|
||||
isnotnull = 70, // SQLITE_INDEX_CONSTRAINT_ISNOTNULL
|
||||
isnull = 71, // SQLITE_INDEX_CONSTRAINT_ISNULL
|
||||
is = 72, // SQLITE_INDEX_CONSTRAINT_IS
|
||||
limit = 73, // SQLITE_INDEX_CONSTRAINT_LIMIT
|
||||
offset = 74, // SQLITE_INDEX_CONSTRAINT_OFFSET
|
||||
function = 150, // SQLITE_INDEX_CONSTRAINT_FUNCTION
|
||||
};
|
||||
|
||||
/// Constraint information for query planning.
|
||||
pub const IndexConstraint = struct {
|
||||
column: i32,
|
||||
op: ConstraintOp,
|
||||
usable: bool,
|
||||
};
|
||||
|
||||
/// Order by information for query planning.
|
||||
pub const IndexOrderBy = struct {
|
||||
column: i32,
|
||||
desc: bool,
|
||||
};
|
||||
|
||||
/// Index info passed to xBestIndex.
|
||||
pub const IndexInfo = struct {
|
||||
constraints: []IndexConstraint,
|
||||
order_by: []IndexOrderBy,
|
||||
// Output fields set by xBestIndex
|
||||
idx_num: i32 = 0,
|
||||
idx_str: ?[]const u8 = null,
|
||||
order_by_consumed: bool = false,
|
||||
estimated_cost: f64 = 1000000.0,
|
||||
estimated_rows: i64 = 1000000,
|
||||
};
|
||||
|
||||
/// Column type declaration for virtual table schema.
|
||||
pub const ColumnDef = struct {
|
||||
name: []const u8,
|
||||
col_type: []const u8,
|
||||
constraints: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
/// Virtual table module definition.
|
||||
/// Implement this interface to create a custom virtual table.
|
||||
pub fn VTableModule(comptime Context: type, comptime Cursor: type) type {
|
||||
return struct {
|
||||
/// Called when the virtual table is created.
|
||||
/// Should return the column definitions for CREATE TABLE.
|
||||
create_fn: *const fn (db: *Database, args: []const []const u8) anyerror!Context,
|
||||
|
||||
/// Called when the virtual table is connected (opened).
|
||||
connect_fn: *const fn (db: *Database, args: []const []const u8) anyerror!Context,
|
||||
|
||||
/// Called to query the best index for a query.
|
||||
best_index_fn: *const fn (ctx: *Context, info: *IndexInfo) anyerror!void,
|
||||
|
||||
/// Called when the virtual table is destroyed.
|
||||
destroy_fn: *const fn (ctx: *Context) void,
|
||||
|
||||
/// Called to open a cursor for scanning.
|
||||
open_fn: *const fn (ctx: *Context) anyerror!Cursor,
|
||||
|
||||
/// Called to close a cursor.
|
||||
close_fn: *const fn (cursor: *Cursor) void,
|
||||
|
||||
/// Called to start a query with the chosen index.
|
||||
filter_fn: *const fn (cursor: *Cursor, idx_num: i32, idx_str: ?[]const u8, args: []const ?*c.sqlite3_value) anyerror!void,
|
||||
|
||||
/// Called to advance to the next row.
|
||||
next_fn: *const fn (cursor: *Cursor) anyerror!void,
|
||||
|
||||
/// Called to check if we've reached end of data.
|
||||
eof_fn: *const fn (cursor: *Cursor) bool,
|
||||
|
||||
/// Called to get a column value.
|
||||
column_fn: *const fn (cursor: *Cursor, col_idx: i32, result_ctx: *c.sqlite3_context) anyerror!void,
|
||||
|
||||
/// Called to get the rowid.
|
||||
rowid_fn: *const fn (cursor: *Cursor) anyerror!i64,
|
||||
|
||||
/// Optional: Called to update a row.
|
||||
update_fn: ?*const fn (ctx: *Context, argc: i32, argv: [*]?*c.sqlite3_value, rowid: *i64) anyerror!void = null,
|
||||
|
||||
/// Returns the schema for this virtual table.
|
||||
schema_fn: *const fn (ctx: *Context) []const ColumnDef,
|
||||
};
|
||||
}
|
||||
|
||||
/// Simple read-only virtual table helper.
|
||||
/// Easier to use than the full VTableModule for simple cases.
|
||||
pub fn SimpleVTable(comptime Row: type) type {
|
||||
return struct {
|
||||
data: []const Row,
|
||||
columns: []const ColumnDef,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn init(data: []const Row, columns: []const ColumnDef) Self {
|
||||
return .{
|
||||
.data = data,
|
||||
.columns = columns,
|
||||
};
|
||||
}
|
||||
|
||||
/// Cursor for iterating over rows.
|
||||
pub const Cursor = struct {
|
||||
table: *const Self,
|
||||
current_row: usize,
|
||||
|
||||
pub fn next(self: *Cursor) void {
|
||||
self.current_row += 1;
|
||||
}
|
||||
|
||||
pub fn eof(self: *const Cursor) bool {
|
||||
return self.current_row >= self.table.data.len;
|
||||
}
|
||||
|
||||
pub fn currentRow(self: *const Cursor) ?Row {
|
||||
if (self.eof()) return null;
|
||||
return self.table.data[self.current_row];
|
||||
}
|
||||
|
||||
pub fn rowid(self: *const Cursor) i64 {
|
||||
return @intCast(self.current_row + 1);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn openCursor(self: *const Self) Cursor {
|
||||
return .{
|
||||
.table = self,
|
||||
.current_row = 0,
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Generates CREATE TABLE statement for a virtual table schema.
|
||||
pub fn generateSchema(allocator: std.mem.Allocator, table_name: []const u8, columns: []const ColumnDef) ![]u8 {
|
||||
var sql = std.ArrayList(u8).init(allocator);
|
||||
errdefer sql.deinit();
|
||||
|
||||
try sql.appendSlice("CREATE TABLE ");
|
||||
try sql.appendSlice(table_name);
|
||||
try sql.appendSlice("(");
|
||||
|
||||
for (columns, 0..) |col, i| {
|
||||
if (i > 0) try sql.appendSlice(", ");
|
||||
try sql.appendSlice(col.name);
|
||||
try sql.append(' ');
|
||||
try sql.appendSlice(col.col_type);
|
||||
if (col.constraints) |cons| {
|
||||
try sql.append(' ');
|
||||
try sql.appendSlice(cons);
|
||||
}
|
||||
}
|
||||
|
||||
try sql.appendSlice(")");
|
||||
return sql.toOwnedSlice();
|
||||
}
|
||||
|
||||
/// Helper to set a result value in a sqlite3_context.
|
||||
pub const ResultHelper = struct {
|
||||
ctx: *c.sqlite3_context,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn init(ctx: *c.sqlite3_context) Self {
|
||||
return .{ .ctx = ctx };
|
||||
}
|
||||
|
||||
pub fn setNull(self: Self) void {
|
||||
c.sqlite3_result_null(self.ctx);
|
||||
}
|
||||
|
||||
pub fn setInt(self: Self, value: i64) void {
|
||||
c.sqlite3_result_int64(self.ctx, value);
|
||||
}
|
||||
|
||||
pub fn setFloat(self: Self, value: f64) void {
|
||||
c.sqlite3_result_double(self.ctx, value);
|
||||
}
|
||||
|
||||
pub fn setText(self: Self, value: []const u8) void {
|
||||
c.sqlite3_result_text(self.ctx, value.ptr, @intCast(value.len), c.SQLITE_TRANSIENT);
|
||||
}
|
||||
|
||||
pub fn setBlob(self: Self, value: []const u8) void {
|
||||
c.sqlite3_result_blob(self.ctx, value.ptr, @intCast(value.len), c.SQLITE_TRANSIENT);
|
||||
}
|
||||
|
||||
pub fn setError(self: Self, msg: []const u8) void {
|
||||
c.sqlite3_result_error(self.ctx, msg.ptr, @intCast(msg.len));
|
||||
}
|
||||
|
||||
pub fn setErrorCode(self: Self, code: c_int) void {
|
||||
c.sqlite3_result_error_code(self.ctx, code);
|
||||
}
|
||||
};
|
||||
|
||||
/// Helper to read values from sqlite3_value.
|
||||
pub const ValueHelper = struct {
|
||||
value: *c.sqlite3_value,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn init(value: *c.sqlite3_value) Self {
|
||||
return .{ .value = value };
|
||||
}
|
||||
|
||||
pub fn getType(self: Self) c_int {
|
||||
return c.sqlite3_value_type(self.value);
|
||||
}
|
||||
|
||||
pub fn isNull(self: Self) bool {
|
||||
return self.getType() == c.SQLITE_NULL;
|
||||
}
|
||||
|
||||
pub fn getInt(self: Self) i64 {
|
||||
return c.sqlite3_value_int64(self.value);
|
||||
}
|
||||
|
||||
pub fn getFloat(self: Self) f64 {
|
||||
return c.sqlite3_value_double(self.value);
|
||||
}
|
||||
|
||||
pub fn getText(self: Self) ?[]const u8 {
|
||||
const len = c.sqlite3_value_bytes(self.value);
|
||||
const text = c.sqlite3_value_text(self.value);
|
||||
if (text) |t| {
|
||||
return t[0..@intCast(len)];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn getBlob(self: Self) ?[]const u8 {
|
||||
const len = c.sqlite3_value_bytes(self.value);
|
||||
const blob = c.sqlite3_value_blob(self.value);
|
||||
if (blob) |b| {
|
||||
const ptr: [*]const u8 = @ptrCast(b);
|
||||
return ptr[0..@intCast(len)];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/// Eponymous virtual table - appears automatically without CREATE VIRTUAL TABLE.
|
||||
/// Useful for table-valued functions.
|
||||
pub const EponymousModule = struct {
|
||||
name: []const u8,
|
||||
num_args: i32,
|
||||
|
||||
/// Generates SQL to query an eponymous module.
|
||||
pub fn query(self: *const EponymousModule, allocator: std.mem.Allocator, args: []const []const u8) ![]u8 {
|
||||
var sql = std.ArrayList(u8).init(allocator);
|
||||
errdefer sql.deinit();
|
||||
|
||||
try sql.appendSlice("SELECT * FROM ");
|
||||
try sql.appendSlice(self.name);
|
||||
try sql.append('(');
|
||||
|
||||
for (args, 0..) |arg, i| {
|
||||
if (i > 0) try sql.appendSlice(", ");
|
||||
try sql.append('\'');
|
||||
try sql.appendSlice(arg);
|
||||
try sql.append('\'');
|
||||
}
|
||||
|
||||
try sql.append(')');
|
||||
return sql.toOwnedSlice();
|
||||
}
|
||||
};
|
||||
|
||||
// Note: Full virtual table registration requires complex C callback trampolines
|
||||
// similar to what we do for UDFs. The helpers above provide the building blocks
|
||||
// but the actual registration would need additional C interop code.
|
||||
//
|
||||
// For most use cases, the built-in virtual tables (FTS5, R-Tree, JSON) are
|
||||
// sufficient. Custom virtual tables are an advanced feature that few applications
|
||||
// need.
|
||||
|
||||
/// Common virtual table modules available in SQLite.
|
||||
pub const BuiltinModules = struct {
|
||||
/// FTS5 full-text search
|
||||
pub const fts5 = "fts5";
|
||||
/// R-Tree spatial index
|
||||
pub const rtree = "rtree";
|
||||
/// CSV virtual table (if compiled with)
|
||||
pub const csv = "csv";
|
||||
/// Generate series (built-in table-valued function)
|
||||
pub const generate_series = "generate_series";
|
||||
/// PRAGMA function as table
|
||||
pub const pragma = "pragma";
|
||||
};
|
||||
Loading…
Reference in a new issue