feat: add advanced features - batch bind, row iterator, FTS5, JSON, R-Tree, virtual tables
New features:
- Batch binding: stmt.bindAll(.{ "Alice", 30, 95.5 }), stmt.rebind()
- Row iterator: stmt.iterator(), Row struct with convenient accessors
- File control: setFileControlInt(), getPersistWal(), setChunkSize()
- FTS5 helpers: Fts5 struct with createSimpleTable(), search(), highlight()
- JSON helpers: Json struct with extract(), set(), createArray(), patch()
- R-Tree helpers: RTree struct with insert2D(), queryIntersects2D(), spatial joins
- Virtual table foundations: vtable.zig with helper types
- BoundingBox2D/3D, GeoCoord with distance calculations
Total: 5861 lines across 13 modules
All 54 tests passing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
5e28cbe4bf
commit
167e54530f
7 changed files with 2265 additions and 0 deletions
106
src/database.zig
106
src/database.zig
|
|
@ -792,4 +792,110 @@ pub const Database = struct {
|
|||
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);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
229
src/fts5.zig
Normal file
229
src/fts5.zig
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
//! FTS5 Full-Text Search Helpers
|
||||
//!
|
||||
//! Provides convenient wrappers for SQLite's FTS5 full-text search extension.
|
||||
//! FTS5 is compiled into SQLite by default with -DSQLITE_ENABLE_FTS5.
|
||||
|
||||
const std = @import("std");
|
||||
const Database = @import("database.zig").Database;
|
||||
const Statement = @import("statement.zig").Statement;
|
||||
const Error = @import("errors.zig").Error;
|
||||
|
||||
/// FTS5 tokenizer options.
|
||||
pub const TokenizerOption = enum {
|
||||
unicode61,
|
||||
ascii,
|
||||
porter,
|
||||
trigram,
|
||||
|
||||
pub fn name(self: TokenizerOption) []const u8 {
|
||||
return switch (self) {
|
||||
.unicode61 => "unicode61",
|
||||
.ascii => "ascii",
|
||||
.porter => "porter",
|
||||
.trigram => "trigram",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// FTS5 helper functions for a database connection.
|
||||
pub const Fts5 = struct {
|
||||
db: *Database,
|
||||
allocator: std.mem.Allocator,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
/// Creates a new FTS5 helper bound to a database connection.
|
||||
pub fn init(db: *Database, allocator: std.mem.Allocator) Self {
|
||||
return .{ .db = db, .allocator = allocator };
|
||||
}
|
||||
|
||||
/// Creates a simple FTS5 table with specified columns.
|
||||
pub fn createSimpleTable(self: *Self, table_name: []const u8, columns: []const []const u8) !void {
|
||||
try self.createTableWithTokenizer(table_name, columns, .unicode61);
|
||||
}
|
||||
|
||||
/// Creates an FTS5 table with a specific tokenizer.
|
||||
pub fn createTableWithTokenizer(
|
||||
self: *Self,
|
||||
table_name: []const u8,
|
||||
columns: []const []const u8,
|
||||
tokenizer: TokenizerOption,
|
||||
) !void {
|
||||
// Build column list
|
||||
var col_buf: [4096]u8 = undefined;
|
||||
var col_len: usize = 0;
|
||||
|
||||
for (columns, 0..) |col, i| {
|
||||
if (i > 0) {
|
||||
col_buf[col_len] = ',';
|
||||
col_buf[col_len + 1] = ' ';
|
||||
col_len += 2;
|
||||
}
|
||||
@memcpy(col_buf[col_len..][0..col.len], col);
|
||||
col_len += col.len;
|
||||
}
|
||||
|
||||
const sql = try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"CREATE VIRTUAL TABLE {s} USING fts5({s}, tokenize='{s}')\x00",
|
||||
.{ table_name, col_buf[0..col_len], tokenizer.name() },
|
||||
);
|
||||
defer self.allocator.free(sql);
|
||||
try self.db.exec(sql[0 .. sql.len - 1 :0]);
|
||||
}
|
||||
|
||||
/// Drops an FTS5 table.
|
||||
pub fn dropTable(self: *Self, table_name: []const u8) !void {
|
||||
const sql = try std.fmt.allocPrint(self.allocator, "DROP TABLE IF EXISTS {s}\x00", .{table_name});
|
||||
defer self.allocator.free(sql);
|
||||
try self.db.exec(sql[0 .. sql.len - 1 :0]);
|
||||
}
|
||||
|
||||
/// Performs a full-text search query.
|
||||
pub fn search(
|
||||
self: *Self,
|
||||
table_name: []const u8,
|
||||
query: []const u8,
|
||||
result_columns: []const []const u8,
|
||||
limit: ?u32,
|
||||
) !Statement {
|
||||
// Build column list
|
||||
var col_buf: [4096]u8 = undefined;
|
||||
var col_len: usize = 0;
|
||||
|
||||
if (result_columns.len == 0) {
|
||||
col_buf[0] = '*';
|
||||
col_len = 1;
|
||||
} else {
|
||||
for (result_columns, 0..) |col, i| {
|
||||
if (i > 0) {
|
||||
col_buf[col_len] = ',';
|
||||
col_buf[col_len + 1] = ' ';
|
||||
col_len += 2;
|
||||
}
|
||||
@memcpy(col_buf[col_len..][0..col.len], col);
|
||||
col_len += col.len;
|
||||
}
|
||||
}
|
||||
|
||||
const sql = if (limit) |l|
|
||||
try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"SELECT {s} FROM {s} WHERE {s} MATCH ? ORDER BY rank LIMIT {d}\x00",
|
||||
.{ col_buf[0..col_len], table_name, table_name, l },
|
||||
)
|
||||
else
|
||||
try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"SELECT {s} FROM {s} WHERE {s} MATCH ? ORDER BY rank\x00",
|
||||
.{ col_buf[0..col_len], table_name, table_name },
|
||||
);
|
||||
defer self.allocator.free(sql);
|
||||
|
||||
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
|
||||
try stmt.bindText(1, query);
|
||||
return stmt;
|
||||
}
|
||||
|
||||
/// Gets highlighted snippet from search results.
|
||||
pub fn searchWithHighlight(
|
||||
self: *Self,
|
||||
table_name: []const u8,
|
||||
query: []const u8,
|
||||
column_index: u32,
|
||||
before_match: []const u8,
|
||||
after_match: []const u8,
|
||||
limit: ?u32,
|
||||
) !Statement {
|
||||
const sql = if (limit) |l|
|
||||
try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"SELECT highlight({s}, {d}, '{s}', '{s}') FROM {s} WHERE {s} MATCH ? ORDER BY rank LIMIT {d}\x00",
|
||||
.{ table_name, column_index, before_match, after_match, table_name, table_name, l },
|
||||
)
|
||||
else
|
||||
try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"SELECT highlight({s}, {d}, '{s}', '{s}') FROM {s} WHERE {s} MATCH ? ORDER BY rank\x00",
|
||||
.{ table_name, column_index, before_match, after_match, table_name, table_name },
|
||||
);
|
||||
defer self.allocator.free(sql);
|
||||
|
||||
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
|
||||
try stmt.bindText(1, query);
|
||||
return stmt;
|
||||
}
|
||||
|
||||
/// Gets a snippet (context around matches) from search results.
|
||||
pub fn searchWithSnippet(
|
||||
self: *Self,
|
||||
table_name: []const u8,
|
||||
query: []const u8,
|
||||
column_index: i32,
|
||||
before_match: []const u8,
|
||||
after_match: []const u8,
|
||||
ellipsis: []const u8,
|
||||
max_tokens: u32,
|
||||
limit: ?u32,
|
||||
) !Statement {
|
||||
const sql = if (limit) |l|
|
||||
try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"SELECT snippet({s}, {d}, '{s}', '{s}', '{s}', {d}) FROM {s} WHERE {s} MATCH ? ORDER BY rank LIMIT {d}\x00",
|
||||
.{ table_name, column_index, before_match, after_match, ellipsis, max_tokens, table_name, table_name, l },
|
||||
)
|
||||
else
|
||||
try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"SELECT snippet({s}, {d}, '{s}', '{s}', '{s}', {d}) FROM {s} WHERE {s} MATCH ? ORDER BY rank\x00",
|
||||
.{ table_name, column_index, before_match, after_match, ellipsis, max_tokens, table_name, table_name },
|
||||
);
|
||||
defer self.allocator.free(sql);
|
||||
|
||||
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
|
||||
try stmt.bindText(1, query);
|
||||
return stmt;
|
||||
}
|
||||
|
||||
/// Rebuilds the FTS5 index.
|
||||
pub fn rebuild(self: *Self, table_name: []const u8) !void {
|
||||
const sql = try std.fmt.allocPrint(self.allocator, "INSERT INTO {s}({s}) VALUES('rebuild')\x00", .{ table_name, table_name });
|
||||
defer self.allocator.free(sql);
|
||||
try self.db.exec(sql[0 .. sql.len - 1 :0]);
|
||||
}
|
||||
|
||||
/// Optimizes the FTS5 index by merging segments.
|
||||
pub fn optimize(self: *Self, table_name: []const u8) !void {
|
||||
const sql = try std.fmt.allocPrint(self.allocator, "INSERT INTO {s}({s}) VALUES('optimize')\x00", .{ table_name, table_name });
|
||||
defer self.allocator.free(sql);
|
||||
try self.db.exec(sql[0 .. sql.len - 1 :0]);
|
||||
}
|
||||
|
||||
/// Runs integrity check on the FTS5 index.
|
||||
pub fn integrityCheck(self: *Self, table_name: []const u8) !void {
|
||||
const sql = try std.fmt.allocPrint(self.allocator, "INSERT INTO {s}({s}) VALUES('integrity-check')\x00", .{ table_name, table_name });
|
||||
defer self.allocator.free(sql);
|
||||
try self.db.exec(sql[0 .. sql.len - 1 :0]);
|
||||
}
|
||||
|
||||
/// Deletes all content from the FTS5 table.
|
||||
pub fn deleteAll(self: *Self, table_name: []const u8) !void {
|
||||
const sql = try std.fmt.allocPrint(self.allocator, "DELETE FROM {s}\x00", .{table_name});
|
||||
defer self.allocator.free(sql);
|
||||
try self.db.exec(sql[0 .. sql.len - 1 :0]);
|
||||
}
|
||||
|
||||
/// Gets the total number of rows in the FTS5 table.
|
||||
pub fn count(self: *Self, table_name: []const u8) !i64 {
|
||||
const sql = try std.fmt.allocPrint(self.allocator, "SELECT COUNT(*) FROM {s}\x00", .{table_name});
|
||||
defer self.allocator.free(sql);
|
||||
|
||||
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
|
||||
defer stmt.finalize();
|
||||
|
||||
if (try stmt.step()) {
|
||||
return stmt.columnInt(0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
437
src/json.zig
Normal file
437
src/json.zig
Normal file
|
|
@ -0,0 +1,437 @@
|
|||
//! JSON1 Extension Helpers
|
||||
//!
|
||||
//! Provides convenient wrappers for SQLite's JSON1 extension functions.
|
||||
//! JSON1 is compiled into SQLite by default with -DSQLITE_ENABLE_JSON1.
|
||||
|
||||
const std = @import("std");
|
||||
const Database = @import("database.zig").Database;
|
||||
const Statement = @import("statement.zig").Statement;
|
||||
const Error = @import("errors.zig").Error;
|
||||
|
||||
/// JSON helper functions for a database connection.
|
||||
pub const Json = struct {
|
||||
db: *Database,
|
||||
allocator: std.mem.Allocator,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
/// Creates a new JSON helper bound to a database connection.
|
||||
pub fn init(db: *Database, allocator: std.mem.Allocator) Self {
|
||||
return .{ .db = db, .allocator = allocator };
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// JSON Validation and Parsing
|
||||
// ========================================================================
|
||||
|
||||
/// Validates and minifies a JSON string.
|
||||
pub fn validate(self: *Self, json_text: []const u8) !?[]u8 {
|
||||
var stmt = try self.db.prepare("SELECT json(?)");
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindText(1, json_text);
|
||||
|
||||
if (try stmt.step()) {
|
||||
if (stmt.columnText(0)) |text| {
|
||||
return try self.allocator.dupe(u8, text);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Checks if a string is valid JSON.
|
||||
pub fn isValid(self: *Self, json_text: []const u8) !bool {
|
||||
var stmt = try self.db.prepare("SELECT json_valid(?)");
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindText(1, json_text);
|
||||
|
||||
if (try stmt.step()) {
|
||||
return stmt.columnInt(0) == 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Returns the JSON type of a value at a path.
|
||||
pub fn getType(self: *Self, json_text: []const u8, path: []const u8) !?[]u8 {
|
||||
var stmt = try self.db.prepare("SELECT json_type(?, ?)");
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindText(1, json_text);
|
||||
try stmt.bindText(2, path);
|
||||
|
||||
if (try stmt.step()) {
|
||||
if (stmt.columnText(0)) |text| {
|
||||
return try self.allocator.dupe(u8, text);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// JSON Extraction
|
||||
// ========================================================================
|
||||
|
||||
/// Extracts a value from JSON at the given path.
|
||||
pub fn extract(self: *Self, json_text: []const u8, path: []const u8) !?[]u8 {
|
||||
var stmt = try self.db.prepare("SELECT json_extract(?, ?)");
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindText(1, json_text);
|
||||
try stmt.bindText(2, path);
|
||||
|
||||
if (try stmt.step()) {
|
||||
if (stmt.columnText(0)) |text| {
|
||||
return try self.allocator.dupe(u8, text);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Extracts an integer from JSON at the given path.
|
||||
pub fn extractInt(self: *Self, json_text: []const u8, path: []const u8) !?i64 {
|
||||
var stmt = try self.db.prepare("SELECT json_extract(?, ?)");
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindText(1, json_text);
|
||||
try stmt.bindText(2, path);
|
||||
|
||||
if (try stmt.step()) {
|
||||
if (!stmt.columnIsNull(0)) {
|
||||
return stmt.columnInt(0);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Extracts a float from JSON at the given path.
|
||||
pub fn extractFloat(self: *Self, json_text: []const u8, path: []const u8) !?f64 {
|
||||
var stmt = try self.db.prepare("SELECT json_extract(?, ?)");
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindText(1, json_text);
|
||||
try stmt.bindText(2, path);
|
||||
|
||||
if (try stmt.step()) {
|
||||
if (!stmt.columnIsNull(0)) {
|
||||
return stmt.columnFloat(0);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Extracts a boolean from JSON at the given path.
|
||||
pub fn extractBool(self: *Self, json_text: []const u8, path: []const u8) !?bool {
|
||||
const result = try self.extractInt(json_text, path);
|
||||
if (result) |v| {
|
||||
return v != 0;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// JSON Modification
|
||||
// ========================================================================
|
||||
|
||||
/// Inserts a value into JSON at the given path (only if path doesn't exist).
|
||||
pub fn insert(self: *Self, json_text: []const u8, path: []const u8, value: []const u8) ![]u8 {
|
||||
var stmt = try self.db.prepare("SELECT json_insert(?, ?, json(?))");
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindText(1, json_text);
|
||||
try stmt.bindText(2, path);
|
||||
try stmt.bindText(3, value);
|
||||
|
||||
if (try stmt.step()) {
|
||||
if (stmt.columnText(0)) |text| {
|
||||
return try self.allocator.dupe(u8, text);
|
||||
}
|
||||
}
|
||||
return try self.allocator.dupe(u8, json_text);
|
||||
}
|
||||
|
||||
/// Replaces a value in JSON at the given path (only if path exists).
|
||||
pub fn replace(self: *Self, json_text: []const u8, path: []const u8, value: []const u8) ![]u8 {
|
||||
var stmt = try self.db.prepare("SELECT json_replace(?, ?, json(?))");
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindText(1, json_text);
|
||||
try stmt.bindText(2, path);
|
||||
try stmt.bindText(3, value);
|
||||
|
||||
if (try stmt.step()) {
|
||||
if (stmt.columnText(0)) |text| {
|
||||
return try self.allocator.dupe(u8, text);
|
||||
}
|
||||
}
|
||||
return try self.allocator.dupe(u8, json_text);
|
||||
}
|
||||
|
||||
/// Sets a value in JSON at the given path (insert or replace).
|
||||
pub fn set(self: *Self, json_text: []const u8, path: []const u8, value: []const u8) ![]u8 {
|
||||
var stmt = try self.db.prepare("SELECT json_set(?, ?, json(?))");
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindText(1, json_text);
|
||||
try stmt.bindText(2, path);
|
||||
try stmt.bindText(3, value);
|
||||
|
||||
if (try stmt.step()) {
|
||||
if (stmt.columnText(0)) |text| {
|
||||
return try self.allocator.dupe(u8, text);
|
||||
}
|
||||
}
|
||||
return try self.allocator.dupe(u8, json_text);
|
||||
}
|
||||
|
||||
/// Sets a string value in JSON at the given path.
|
||||
pub fn setString(self: *Self, json_text: []const u8, path: []const u8, value: []const u8) ![]u8 {
|
||||
var stmt = try self.db.prepare("SELECT json_set(?, ?, ?)");
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindText(1, json_text);
|
||||
try stmt.bindText(2, path);
|
||||
try stmt.bindText(3, value);
|
||||
|
||||
if (try stmt.step()) {
|
||||
if (stmt.columnText(0)) |text| {
|
||||
return try self.allocator.dupe(u8, text);
|
||||
}
|
||||
}
|
||||
return try self.allocator.dupe(u8, json_text);
|
||||
}
|
||||
|
||||
/// Sets an integer value in JSON at the given path.
|
||||
pub fn setInt(self: *Self, json_text: []const u8, path: []const u8, value: i64) ![]u8 {
|
||||
var stmt = try self.db.prepare("SELECT json_set(?, ?, ?)");
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindText(1, json_text);
|
||||
try stmt.bindText(2, path);
|
||||
try stmt.bindInt(3, value);
|
||||
|
||||
if (try stmt.step()) {
|
||||
if (stmt.columnText(0)) |text| {
|
||||
return try self.allocator.dupe(u8, text);
|
||||
}
|
||||
}
|
||||
return try self.allocator.dupe(u8, json_text);
|
||||
}
|
||||
|
||||
/// Sets a float value in JSON at the given path.
|
||||
pub fn setFloat(self: *Self, json_text: []const u8, path: []const u8, value: f64) ![]u8 {
|
||||
var stmt = try self.db.prepare("SELECT json_set(?, ?, ?)");
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindText(1, json_text);
|
||||
try stmt.bindText(2, path);
|
||||
try stmt.bindFloat(3, value);
|
||||
|
||||
if (try stmt.step()) {
|
||||
if (stmt.columnText(0)) |text| {
|
||||
return try self.allocator.dupe(u8, text);
|
||||
}
|
||||
}
|
||||
return try self.allocator.dupe(u8, json_text);
|
||||
}
|
||||
|
||||
/// Sets a boolean value in JSON at the given path.
|
||||
pub fn setBool(self: *Self, json_text: []const u8, path: []const u8, value: bool) ![]u8 {
|
||||
const json_bool = if (value) "true" else "false";
|
||||
var stmt = try self.db.prepare("SELECT json_set(?, ?, json(?))");
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindText(1, json_text);
|
||||
try stmt.bindText(2, path);
|
||||
try stmt.bindText(3, json_bool);
|
||||
|
||||
if (try stmt.step()) {
|
||||
if (stmt.columnText(0)) |text| {
|
||||
return try self.allocator.dupe(u8, text);
|
||||
}
|
||||
}
|
||||
return try self.allocator.dupe(u8, json_text);
|
||||
}
|
||||
|
||||
/// Removes a value from JSON at the given path.
|
||||
pub fn remove(self: *Self, json_text: []const u8, path: []const u8) ![]u8 {
|
||||
var stmt = try self.db.prepare("SELECT json_remove(?, ?)");
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindText(1, json_text);
|
||||
try stmt.bindText(2, path);
|
||||
|
||||
if (try stmt.step()) {
|
||||
if (stmt.columnText(0)) |text| {
|
||||
return try self.allocator.dupe(u8, text);
|
||||
}
|
||||
}
|
||||
return try self.allocator.dupe(u8, json_text);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// JSON Array Operations
|
||||
// ========================================================================
|
||||
|
||||
/// Returns the length of a JSON array.
|
||||
pub fn arrayLength(self: *Self, json_text: []const u8, path: ?[]const u8) !?i64 {
|
||||
if (path) |p| {
|
||||
var stmt = try self.db.prepare("SELECT json_array_length(?, ?)");
|
||||
defer stmt.finalize();
|
||||
try stmt.bindText(1, json_text);
|
||||
try stmt.bindText(2, p);
|
||||
if (try stmt.step()) {
|
||||
if (!stmt.columnIsNull(0)) {
|
||||
return stmt.columnInt(0);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var stmt = try self.db.prepare("SELECT json_array_length(?)");
|
||||
defer stmt.finalize();
|
||||
try stmt.bindText(1, json_text);
|
||||
if (try stmt.step()) {
|
||||
if (!stmt.columnIsNull(0)) {
|
||||
return stmt.columnInt(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Creates a JSON array from string values.
|
||||
pub fn createArray(self: *Self, values: []const []const u8) ![]u8 {
|
||||
if (values.len == 0) {
|
||||
return try self.allocator.dupe(u8, "[]");
|
||||
}
|
||||
|
||||
// Build placeholders
|
||||
var placeholders: [256]u8 = undefined;
|
||||
var ph_len: usize = 0;
|
||||
for (0..values.len) |i| {
|
||||
if (i > 0) {
|
||||
placeholders[ph_len] = ',';
|
||||
placeholders[ph_len + 1] = ' ';
|
||||
ph_len += 2;
|
||||
}
|
||||
placeholders[ph_len] = '?';
|
||||
ph_len += 1;
|
||||
}
|
||||
|
||||
const sql = try std.fmt.allocPrint(self.allocator, "SELECT json_array({s})\x00", .{placeholders[0..ph_len]});
|
||||
defer self.allocator.free(sql);
|
||||
|
||||
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
|
||||
defer stmt.finalize();
|
||||
|
||||
for (values, 0..) |v, i| {
|
||||
try stmt.bindText(@intCast(i + 1), v);
|
||||
}
|
||||
|
||||
if (try stmt.step()) {
|
||||
if (stmt.columnText(0)) |text| {
|
||||
return try self.allocator.dupe(u8, text);
|
||||
}
|
||||
}
|
||||
return try self.allocator.dupe(u8, "[]");
|
||||
}
|
||||
|
||||
/// Creates a JSON object from key-value pairs.
|
||||
pub fn createObject(self: *Self, keys: []const []const u8, values: []const []const u8) ![]u8 {
|
||||
if (keys.len != values.len) {
|
||||
return error.OutOfMemory; // Use a generic error
|
||||
}
|
||||
|
||||
if (keys.len == 0) {
|
||||
return try self.allocator.dupe(u8, "{}");
|
||||
}
|
||||
|
||||
// Build placeholders
|
||||
var placeholders: [512]u8 = undefined;
|
||||
var ph_len: usize = 0;
|
||||
for (0..keys.len) |i| {
|
||||
if (i > 0) {
|
||||
placeholders[ph_len] = ',';
|
||||
placeholders[ph_len + 1] = ' ';
|
||||
ph_len += 2;
|
||||
}
|
||||
placeholders[ph_len] = '?';
|
||||
placeholders[ph_len + 1] = ',';
|
||||
placeholders[ph_len + 2] = ' ';
|
||||
placeholders[ph_len + 3] = '?';
|
||||
ph_len += 4;
|
||||
}
|
||||
|
||||
const sql = try std.fmt.allocPrint(self.allocator, "SELECT json_object({s})\x00", .{placeholders[0..ph_len]});
|
||||
defer self.allocator.free(sql);
|
||||
|
||||
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
|
||||
defer stmt.finalize();
|
||||
|
||||
var param_idx: u32 = 1;
|
||||
for (keys, values) |k, v| {
|
||||
try stmt.bindText(param_idx, k);
|
||||
param_idx += 1;
|
||||
try stmt.bindText(param_idx, v);
|
||||
param_idx += 1;
|
||||
}
|
||||
|
||||
if (try stmt.step()) {
|
||||
if (stmt.columnText(0)) |text| {
|
||||
return try self.allocator.dupe(u8, text);
|
||||
}
|
||||
}
|
||||
return try self.allocator.dupe(u8, "{}");
|
||||
}
|
||||
|
||||
/// Applies a JSON patch (RFC 7396) to a JSON document.
|
||||
pub fn patch(self: *Self, target: []const u8, patch_doc: []const u8) ![]u8 {
|
||||
var stmt = try self.db.prepare("SELECT json_patch(?, ?)");
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindText(1, target);
|
||||
try stmt.bindText(2, patch_doc);
|
||||
|
||||
if (try stmt.step()) {
|
||||
if (stmt.columnText(0)) |text| {
|
||||
return try self.allocator.dupe(u8, text);
|
||||
}
|
||||
}
|
||||
return try self.allocator.dupe(u8, target);
|
||||
}
|
||||
|
||||
/// Returns a statement for iterating over JSON elements with json_each.
|
||||
pub fn each(self: *Self, json_text: []const u8, path: ?[]const u8) !Statement {
|
||||
if (path) |p| {
|
||||
var stmt = try self.db.prepare(
|
||||
"SELECT key, value, type, atom, id, parent, fullkey, path FROM json_each(?, ?)",
|
||||
);
|
||||
try stmt.bindText(1, json_text);
|
||||
try stmt.bindText(2, p);
|
||||
return stmt;
|
||||
} else {
|
||||
var stmt = try self.db.prepare(
|
||||
"SELECT key, value, type, atom, id, parent, fullkey, path FROM json_each(?)",
|
||||
);
|
||||
try stmt.bindText(1, json_text);
|
||||
return stmt;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a statement for iterating over all JSON elements with json_tree.
|
||||
pub fn tree(self: *Self, json_text: []const u8, path: ?[]const u8) !Statement {
|
||||
if (path) |p| {
|
||||
var stmt = try self.db.prepare(
|
||||
"SELECT key, value, type, atom, id, parent, fullkey, path FROM json_tree(?, ?)",
|
||||
);
|
||||
try stmt.bindText(1, json_text);
|
||||
try stmt.bindText(2, p);
|
||||
return stmt;
|
||||
} else {
|
||||
var stmt = try self.db.prepare(
|
||||
"SELECT key, value, type, atom, id, parent, fullkey, path FROM json_tree(?)",
|
||||
);
|
||||
try stmt.bindText(1, json_text);
|
||||
return stmt;
|
||||
}
|
||||
}
|
||||
};
|
||||
295
src/root.zig
295
src/root.zig
|
|
@ -34,6 +34,12 @@ const functions_mod = @import("functions.zig");
|
|||
const backup_mod = @import("backup.zig");
|
||||
const pool_mod = @import("pool.zig");
|
||||
|
||||
// Extension helpers
|
||||
pub const fts5 = @import("fts5.zig");
|
||||
pub const json = @import("json.zig");
|
||||
pub const rtree = @import("rtree.zig");
|
||||
pub const vtable = @import("vtable.zig");
|
||||
|
||||
// Re-export C bindings (for advanced users)
|
||||
pub const c = c_mod.c;
|
||||
|
||||
|
|
@ -57,6 +63,18 @@ pub const Backup = backup_mod.Backup;
|
|||
pub const Blob = backup_mod.Blob;
|
||||
pub const ConnectionPool = pool_mod.ConnectionPool;
|
||||
|
||||
// Re-export iterator types
|
||||
pub const RowIterator = statement_mod.RowIterator;
|
||||
pub const Row = statement_mod.Row;
|
||||
|
||||
// Re-export extension helpers (convenience aliases)
|
||||
pub const Fts5 = fts5.Fts5;
|
||||
pub const Json = json.Json;
|
||||
pub const RTree = rtree.RTree;
|
||||
pub const BoundingBox2D = rtree.BoundingBox2D;
|
||||
pub const BoundingBox3D = rtree.BoundingBox3D;
|
||||
pub const GeoCoord = rtree.GeoCoord;
|
||||
|
||||
// Re-export function types
|
||||
pub const FunctionContext = functions_mod.FunctionContext;
|
||||
pub const FunctionValue = functions_mod.FunctionValue;
|
||||
|
|
@ -1091,3 +1109,280 @@ test "connection pool reuse" {
|
|||
|
||||
pool.release(conn2);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// New feature tests: Batch bind, Row iterator, FTS5, JSON, R-Tree
|
||||
// ============================================================================
|
||||
|
||||
test "batch bind with tuple" {
|
||||
var db = try openMemory();
|
||||
defer db.close();
|
||||
|
||||
try db.exec("CREATE TABLE users (name TEXT, age INTEGER, score REAL)");
|
||||
|
||||
var stmt = try db.prepare("INSERT INTO users VALUES (?, ?, ?)");
|
||||
defer stmt.finalize();
|
||||
|
||||
// Bind all values from a tuple
|
||||
try stmt.bindAll(.{ "Alice", @as(i64, 30), @as(f64, 95.5) });
|
||||
_ = try stmt.step();
|
||||
|
||||
// Rebind for second insert
|
||||
try stmt.rebind(.{ "Bob", @as(i64, 25), @as(f64, 88.0) });
|
||||
_ = try stmt.step();
|
||||
|
||||
// Verify
|
||||
var query = try db.prepare("SELECT name, age, score FROM users ORDER BY name");
|
||||
defer query.finalize();
|
||||
|
||||
_ = try query.step();
|
||||
try std.testing.expectEqualStrings("Alice", query.columnText(0).?);
|
||||
try std.testing.expectEqual(@as(i64, 30), query.columnInt(1));
|
||||
try std.testing.expectApproxEqAbs(@as(f64, 95.5), query.columnFloat(2), 0.01);
|
||||
|
||||
_ = try query.step();
|
||||
try std.testing.expectEqualStrings("Bob", query.columnText(0).?);
|
||||
}
|
||||
|
||||
test "batch bind with optional null" {
|
||||
var db = try openMemory();
|
||||
defer db.close();
|
||||
|
||||
try db.exec("CREATE TABLE test (a TEXT, b INTEGER)");
|
||||
|
||||
var stmt = try db.prepare("INSERT INTO test VALUES (?, ?)");
|
||||
defer stmt.finalize();
|
||||
|
||||
// Bind with optional null
|
||||
const optional_text: ?[]const u8 = null;
|
||||
try stmt.bindAll(.{ optional_text, @as(i64, 42) });
|
||||
_ = try stmt.step();
|
||||
|
||||
var query = try db.prepare("SELECT a, b FROM test");
|
||||
defer query.finalize();
|
||||
|
||||
_ = try query.step();
|
||||
try std.testing.expect(query.columnIsNull(0));
|
||||
try std.testing.expectEqual(@as(i64, 42), query.columnInt(1));
|
||||
}
|
||||
|
||||
test "row iterator" {
|
||||
var db = try openMemory();
|
||||
defer db.close();
|
||||
|
||||
try db.exec("CREATE TABLE items (id INTEGER, name TEXT)");
|
||||
try db.exec("INSERT INTO items VALUES (1, 'Apple'), (2, 'Banana'), (3, 'Cherry')");
|
||||
|
||||
var stmt = try db.prepare("SELECT id, name FROM items ORDER BY id");
|
||||
defer stmt.finalize();
|
||||
|
||||
var iter = stmt.iterator();
|
||||
var count: i32 = 0;
|
||||
|
||||
while (try iter.next()) |row| {
|
||||
count += 1;
|
||||
const id = row.int(0);
|
||||
const name = row.text(1).?;
|
||||
|
||||
switch (count) {
|
||||
1 => {
|
||||
try std.testing.expectEqual(@as(i64, 1), id);
|
||||
try std.testing.expectEqualStrings("Apple", name);
|
||||
},
|
||||
2 => {
|
||||
try std.testing.expectEqual(@as(i64, 2), id);
|
||||
try std.testing.expectEqualStrings("Banana", name);
|
||||
},
|
||||
3 => {
|
||||
try std.testing.expectEqual(@as(i64, 3), id);
|
||||
try std.testing.expectEqualStrings("Cherry", name);
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
try std.testing.expectEqual(@as(i32, 3), count);
|
||||
}
|
||||
|
||||
test "FTS5 basic search" {
|
||||
const allocator = std.testing.allocator;
|
||||
var db = try openMemory();
|
||||
defer db.close();
|
||||
|
||||
var fts = Fts5.init(&db, allocator);
|
||||
|
||||
// Create FTS5 table
|
||||
try fts.createSimpleTable("documents", &.{ "title", "body" });
|
||||
|
||||
// Insert documents
|
||||
try db.exec("INSERT INTO documents VALUES ('Hello World', 'This is a test document')");
|
||||
try db.exec("INSERT INTO documents VALUES ('Zig Programming', 'Zig is a systems programming language')");
|
||||
try db.exec("INSERT INTO documents VALUES ('SQLite Guide', 'Learn about SQLite database')");
|
||||
|
||||
// Search
|
||||
var stmt = try fts.search("documents", "programming", &.{ "title", "body" }, 10);
|
||||
defer stmt.finalize();
|
||||
|
||||
var found = false;
|
||||
while (try stmt.step()) {
|
||||
const title = stmt.columnText(0).?;
|
||||
if (std.mem.eql(u8, title, "Zig Programming")) {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
try std.testing.expect(found);
|
||||
}
|
||||
|
||||
test "FTS5 highlight" {
|
||||
const allocator = std.testing.allocator;
|
||||
var db = try openMemory();
|
||||
defer db.close();
|
||||
|
||||
var fts = Fts5.init(&db, allocator);
|
||||
try fts.createSimpleTable("docs", &.{"content"});
|
||||
|
||||
try db.exec("INSERT INTO docs VALUES ('The quick brown fox jumps over the lazy dog')");
|
||||
|
||||
var stmt = try fts.searchWithHighlight("docs", "quick", 0, "<b>", "</b>", 10);
|
||||
defer stmt.finalize();
|
||||
|
||||
if (try stmt.step()) {
|
||||
const highlighted = stmt.columnText(0).?;
|
||||
try std.testing.expect(std.mem.indexOf(u8, highlighted, "<b>quick</b>") != null);
|
||||
}
|
||||
}
|
||||
|
||||
test "JSON validate and extract" {
|
||||
const allocator = std.testing.allocator;
|
||||
var db = try openMemory();
|
||||
defer db.close();
|
||||
|
||||
var js = Json.init(&db, allocator);
|
||||
|
||||
// Validate JSON
|
||||
const valid = try js.isValid("{\"name\": \"Alice\", \"age\": 30}");
|
||||
try std.testing.expect(valid);
|
||||
|
||||
const invalid = try js.isValid("{invalid json}");
|
||||
try std.testing.expect(!invalid);
|
||||
|
||||
// Extract values
|
||||
const json_data = "{\"name\": \"Bob\", \"age\": 25, \"active\": true}";
|
||||
|
||||
if (try js.extract(json_data, "$.name")) |name| {
|
||||
defer allocator.free(name);
|
||||
try std.testing.expectEqualStrings("Bob", name);
|
||||
}
|
||||
|
||||
const age = try js.extractInt(json_data, "$.age");
|
||||
try std.testing.expectEqual(@as(?i64, 25), age);
|
||||
}
|
||||
|
||||
test "JSON modify" {
|
||||
const allocator = std.testing.allocator;
|
||||
var db = try openMemory();
|
||||
defer db.close();
|
||||
|
||||
var js = Json.init(&db, allocator);
|
||||
|
||||
const original = "{\"name\": \"Alice\"}";
|
||||
|
||||
// Set a new field
|
||||
const modified = try js.setInt(original, "$.age", 30);
|
||||
defer allocator.free(modified);
|
||||
|
||||
// Verify the modification
|
||||
const age = try js.extractInt(modified, "$.age");
|
||||
try std.testing.expectEqual(@as(?i64, 30), age);
|
||||
}
|
||||
|
||||
test "JSON array operations" {
|
||||
const allocator = std.testing.allocator;
|
||||
var db = try openMemory();
|
||||
defer db.close();
|
||||
|
||||
var js = Json.init(&db, allocator);
|
||||
|
||||
// Create array
|
||||
const arr = try js.createArray(&.{ "1", "2", "3" });
|
||||
defer allocator.free(arr);
|
||||
|
||||
// Check length
|
||||
const len = try js.arrayLength(arr, null);
|
||||
try std.testing.expectEqual(@as(?i64, 3), len);
|
||||
}
|
||||
|
||||
test "R-Tree basic spatial query" {
|
||||
const allocator = std.testing.allocator;
|
||||
var db = try openMemory();
|
||||
defer db.close();
|
||||
|
||||
var rt = RTree.init(&db, allocator);
|
||||
|
||||
// Create R-Tree table
|
||||
try rt.createSimpleTable2D("locations");
|
||||
|
||||
// Insert some points
|
||||
try rt.insertPoint2D("locations", 1, 10.0, 20.0);
|
||||
try rt.insertPoint2D("locations", 2, 15.0, 25.0);
|
||||
try rt.insertPoint2D("locations", 3, 100.0, 200.0); // Far away
|
||||
|
||||
// Query points near (12, 22) with radius 10
|
||||
const ids = try rt.getIntersectingIds2D("locations", BoundingBox2D{
|
||||
.min_x = 5.0,
|
||||
.max_x = 25.0,
|
||||
.min_y = 15.0,
|
||||
.max_y = 35.0,
|
||||
});
|
||||
defer rt.freeIds(ids);
|
||||
|
||||
// Should find points 1 and 2, but not 3
|
||||
try std.testing.expectEqual(@as(usize, 2), ids.len);
|
||||
}
|
||||
|
||||
test "R-Tree bounding box operations" {
|
||||
const box1 = BoundingBox2D{
|
||||
.min_x = 0.0,
|
||||
.max_x = 10.0,
|
||||
.min_y = 0.0,
|
||||
.max_y = 10.0,
|
||||
};
|
||||
|
||||
const box2 = BoundingBox2D{
|
||||
.min_x = 5.0,
|
||||
.max_x = 15.0,
|
||||
.min_y = 5.0,
|
||||
.max_y = 15.0,
|
||||
};
|
||||
|
||||
const box3 = BoundingBox2D{
|
||||
.min_x = 20.0,
|
||||
.max_x = 30.0,
|
||||
.min_y = 20.0,
|
||||
.max_y = 30.0,
|
||||
};
|
||||
|
||||
// box1 and box2 intersect
|
||||
try std.testing.expect(box1.intersects(box2));
|
||||
|
||||
// box1 and box3 do not intersect
|
||||
try std.testing.expect(!box1.intersects(box3));
|
||||
|
||||
// Point containment
|
||||
try std.testing.expect(box1.containsPoint(5.0, 5.0));
|
||||
try std.testing.expect(!box1.containsPoint(15.0, 15.0));
|
||||
|
||||
// Area
|
||||
try std.testing.expectApproxEqAbs(@as(f64, 100.0), box1.area(), 0.01);
|
||||
}
|
||||
|
||||
test "R-Tree geographic distance" {
|
||||
// Test Haversine distance calculation
|
||||
const london = GeoCoord{ .latitude = 51.5074, .longitude = -0.1278 };
|
||||
const paris = GeoCoord{ .latitude = 48.8566, .longitude = 2.3522 };
|
||||
|
||||
const distance = london.distanceKm(paris);
|
||||
|
||||
// London to Paris is approximately 343 km
|
||||
try std.testing.expect(distance > 340.0 and distance < 350.0);
|
||||
}
|
||||
|
|
|
|||
564
src/rtree.zig
Normal file
564
src/rtree.zig
Normal file
|
|
@ -0,0 +1,564 @@
|
|||
//! R-Tree Spatial Index Helpers
|
||||
//!
|
||||
//! Provides convenient wrappers for SQLite's R-Tree extension for spatial indexing.
|
||||
//! R-Tree is compiled into SQLite by default with -DSQLITE_ENABLE_RTREE.
|
||||
//!
|
||||
//! R-Tree is useful for:
|
||||
//! - Geographic data (latitude/longitude bounding boxes)
|
||||
//! - 2D/3D spatial queries
|
||||
//! - Range queries on multiple dimensions
|
||||
//! - Game collision detection
|
||||
//! - Any multi-dimensional range search
|
||||
|
||||
const std = @import("std");
|
||||
const Database = @import("database.zig").Database;
|
||||
const Statement = @import("statement.zig").Statement;
|
||||
const Error = @import("errors.zig").Error;
|
||||
|
||||
/// A 2D bounding box (rectangle).
|
||||
pub const BoundingBox2D = struct {
|
||||
min_x: f64,
|
||||
max_x: f64,
|
||||
min_y: f64,
|
||||
max_y: f64,
|
||||
|
||||
/// Creates a bounding box from center point and size.
|
||||
pub fn fromCenter(center_x: f64, center_y: f64, width: f64, height: f64) BoundingBox2D {
|
||||
const half_w = width / 2.0;
|
||||
const half_h = height / 2.0;
|
||||
return .{
|
||||
.min_x = center_x - half_w,
|
||||
.max_x = center_x + half_w,
|
||||
.min_y = center_y - half_h,
|
||||
.max_y = center_y + half_h,
|
||||
};
|
||||
}
|
||||
|
||||
/// Creates a bounding box from a point (zero-size box).
|
||||
pub fn fromPoint(x: f64, y: f64) BoundingBox2D {
|
||||
return .{
|
||||
.min_x = x,
|
||||
.max_x = x,
|
||||
.min_y = y,
|
||||
.max_y = y,
|
||||
};
|
||||
}
|
||||
|
||||
/// Checks if this box intersects with another box.
|
||||
pub fn intersects(self: BoundingBox2D, other: BoundingBox2D) bool {
|
||||
return self.min_x <= other.max_x and
|
||||
self.max_x >= other.min_x and
|
||||
self.min_y <= other.max_y and
|
||||
self.max_y >= other.min_y;
|
||||
}
|
||||
|
||||
/// Checks if this box contains a point.
|
||||
pub fn containsPoint(self: BoundingBox2D, x: f64, y: f64) bool {
|
||||
return x >= self.min_x and x <= self.max_x and
|
||||
y >= self.min_y and y <= self.max_y;
|
||||
}
|
||||
|
||||
/// Checks if this box fully contains another box.
|
||||
pub fn contains(self: BoundingBox2D, other: BoundingBox2D) bool {
|
||||
return other.min_x >= self.min_x and
|
||||
other.max_x <= self.max_x and
|
||||
other.min_y >= self.min_y and
|
||||
other.max_y <= self.max_y;
|
||||
}
|
||||
|
||||
/// Returns the area of the bounding box.
|
||||
pub fn area(self: BoundingBox2D) f64 {
|
||||
return (self.max_x - self.min_x) * (self.max_y - self.min_y);
|
||||
}
|
||||
|
||||
/// Returns the center point of the bounding box.
|
||||
pub fn center(self: BoundingBox2D) struct { x: f64, y: f64 } {
|
||||
return .{
|
||||
.x = (self.min_x + self.max_x) / 2.0,
|
||||
.y = (self.min_y + self.max_y) / 2.0,
|
||||
};
|
||||
}
|
||||
|
||||
/// Expands this box to include another box.
|
||||
pub fn expand(self: *BoundingBox2D, other: BoundingBox2D) void {
|
||||
self.min_x = @min(self.min_x, other.min_x);
|
||||
self.max_x = @max(self.max_x, other.max_x);
|
||||
self.min_y = @min(self.min_y, other.min_y);
|
||||
self.max_y = @max(self.max_y, other.max_y);
|
||||
}
|
||||
};
|
||||
|
||||
/// A 3D bounding box.
|
||||
pub const BoundingBox3D = struct {
|
||||
min_x: f64,
|
||||
max_x: f64,
|
||||
min_y: f64,
|
||||
max_y: f64,
|
||||
min_z: f64,
|
||||
max_z: f64,
|
||||
|
||||
/// Creates a 3D bounding box from center point and size.
|
||||
pub fn fromCenter(center_x: f64, center_y: f64, center_z: f64, width: f64, height: f64, depth: f64) BoundingBox3D {
|
||||
const half_w = width / 2.0;
|
||||
const half_h = height / 2.0;
|
||||
const half_d = depth / 2.0;
|
||||
return .{
|
||||
.min_x = center_x - half_w,
|
||||
.max_x = center_x + half_w,
|
||||
.min_y = center_y - half_h,
|
||||
.max_y = center_y + half_h,
|
||||
.min_z = center_z - half_d,
|
||||
.max_z = center_z + half_d,
|
||||
};
|
||||
}
|
||||
|
||||
/// Creates a bounding box from a point (zero-size box).
|
||||
pub fn fromPoint(x: f64, y: f64, z: f64) BoundingBox3D {
|
||||
return .{
|
||||
.min_x = x,
|
||||
.max_x = x,
|
||||
.min_y = y,
|
||||
.max_y = y,
|
||||
.min_z = z,
|
||||
.max_z = z,
|
||||
};
|
||||
}
|
||||
|
||||
/// Checks if this box intersects with another box.
|
||||
pub fn intersects(self: BoundingBox3D, other: BoundingBox3D) bool {
|
||||
return self.min_x <= other.max_x and
|
||||
self.max_x >= other.min_x and
|
||||
self.min_y <= other.max_y and
|
||||
self.max_y >= other.min_y and
|
||||
self.min_z <= other.max_z and
|
||||
self.max_z >= other.min_z;
|
||||
}
|
||||
|
||||
/// Returns the volume of the bounding box.
|
||||
pub fn volume(self: BoundingBox3D) f64 {
|
||||
return (self.max_x - self.min_x) * (self.max_y - self.min_y) * (self.max_z - self.min_z);
|
||||
}
|
||||
};
|
||||
|
||||
/// Geographic coordinates (latitude/longitude).
|
||||
pub const GeoCoord = struct {
|
||||
latitude: f64,
|
||||
longitude: f64,
|
||||
|
||||
/// Creates a bounding box around this point with a given radius in degrees.
|
||||
pub fn boundingBox(self: GeoCoord, radius_degrees: f64) BoundingBox2D {
|
||||
return .{
|
||||
.min_x = self.longitude - radius_degrees,
|
||||
.max_x = self.longitude + radius_degrees,
|
||||
.min_y = self.latitude - radius_degrees,
|
||||
.max_y = self.latitude + radius_degrees,
|
||||
};
|
||||
}
|
||||
|
||||
/// Approximate distance in kilometers to another coordinate (Haversine formula).
|
||||
pub fn distanceKm(self: GeoCoord, other: GeoCoord) f64 {
|
||||
const earth_radius_km = 6371.0;
|
||||
const lat1 = self.latitude * std.math.pi / 180.0;
|
||||
const lat2 = other.latitude * std.math.pi / 180.0;
|
||||
const delta_lat = (other.latitude - self.latitude) * std.math.pi / 180.0;
|
||||
const delta_lon = (other.longitude - self.longitude) * std.math.pi / 180.0;
|
||||
|
||||
const a = std.math.sin(delta_lat / 2.0) * std.math.sin(delta_lat / 2.0) +
|
||||
std.math.cos(lat1) * std.math.cos(lat2) *
|
||||
std.math.sin(delta_lon / 2.0) * std.math.sin(delta_lon / 2.0);
|
||||
const c = 2.0 * std.math.atan2(std.math.sqrt(a), std.math.sqrt(1.0 - a));
|
||||
|
||||
return earth_radius_km * c;
|
||||
}
|
||||
};
|
||||
|
||||
/// R-Tree helper functions for a database connection.
|
||||
pub const RTree = struct {
|
||||
db: *Database,
|
||||
allocator: std.mem.Allocator,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
/// Creates a new R-Tree helper bound to a database connection.
|
||||
pub fn init(db: *Database, allocator: std.mem.Allocator) Self {
|
||||
return .{ .db = db, .allocator = allocator };
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Table Management
|
||||
// ========================================================================
|
||||
|
||||
/// Creates a 2D R-Tree virtual table.
|
||||
///
|
||||
/// Example:
|
||||
/// ```zig
|
||||
/// var rtree = RTree.init(&db, allocator);
|
||||
/// try rtree.createTable2D("locations", "min_x", "max_x", "min_y", "max_y");
|
||||
/// ```
|
||||
pub fn createTable2D(
|
||||
self: *Self,
|
||||
table_name: []const u8,
|
||||
min_x_name: []const u8,
|
||||
max_x_name: []const u8,
|
||||
min_y_name: []const u8,
|
||||
max_y_name: []const u8,
|
||||
) !void {
|
||||
const sql = try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"CREATE VIRTUAL TABLE {s} USING rtree(id, {s}, {s}, {s}, {s})\x00",
|
||||
.{ table_name, min_x_name, max_x_name, min_y_name, max_y_name },
|
||||
);
|
||||
defer self.allocator.free(sql);
|
||||
try self.db.exec(sql[0 .. sql.len - 1 :0]);
|
||||
}
|
||||
|
||||
/// Creates a 3D R-Tree virtual table.
|
||||
pub fn createTable3D(
|
||||
self: *Self,
|
||||
table_name: []const u8,
|
||||
min_x_name: []const u8,
|
||||
max_x_name: []const u8,
|
||||
min_y_name: []const u8,
|
||||
max_y_name: []const u8,
|
||||
min_z_name: []const u8,
|
||||
max_z_name: []const u8,
|
||||
) !void {
|
||||
const sql = try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"CREATE VIRTUAL TABLE {s} USING rtree(id, {s}, {s}, {s}, {s}, {s}, {s})\x00",
|
||||
.{ table_name, min_x_name, max_x_name, min_y_name, max_y_name, min_z_name, max_z_name },
|
||||
);
|
||||
defer self.allocator.free(sql);
|
||||
try self.db.exec(sql[0 .. sql.len - 1 :0]);
|
||||
}
|
||||
|
||||
/// Creates a simple 2D R-Tree with default column names.
|
||||
pub fn createSimpleTable2D(self: *Self, table_name: []const u8) !void {
|
||||
try self.createTable2D(table_name, "min_x", "max_x", "min_y", "max_y");
|
||||
}
|
||||
|
||||
/// Creates a simple 3D R-Tree with default column names.
|
||||
pub fn createSimpleTable3D(self: *Self, table_name: []const u8) !void {
|
||||
try self.createTable3D(table_name, "min_x", "max_x", "min_y", "max_y", "min_z", "max_z");
|
||||
}
|
||||
|
||||
/// Creates a geographic R-Tree (using latitude/longitude).
|
||||
pub fn createGeoTable(self: *Self, table_name: []const u8) !void {
|
||||
try self.createTable2D(table_name, "min_lon", "max_lon", "min_lat", "max_lat");
|
||||
}
|
||||
|
||||
/// Drops an R-Tree table.
|
||||
pub fn dropTable(self: *Self, table_name: []const u8) !void {
|
||||
const sql = try std.fmt.allocPrint(self.allocator, "DROP TABLE IF EXISTS {s}\x00", .{table_name});
|
||||
defer self.allocator.free(sql);
|
||||
try self.db.exec(sql[0 .. sql.len - 1 :0]);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Insert Operations
|
||||
// ========================================================================
|
||||
|
||||
/// Inserts a 2D bounding box into an R-Tree.
|
||||
pub fn insert2D(self: *Self, table_name: []const u8, id: i64, box: BoundingBox2D) !void {
|
||||
const sql = try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"INSERT INTO {s} VALUES (?, ?, ?, ?, ?)\x00",
|
||||
.{table_name},
|
||||
);
|
||||
defer self.allocator.free(sql);
|
||||
|
||||
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindInt(1, id);
|
||||
try stmt.bindFloat(2, box.min_x);
|
||||
try stmt.bindFloat(3, box.max_x);
|
||||
try stmt.bindFloat(4, box.min_y);
|
||||
try stmt.bindFloat(5, box.max_y);
|
||||
|
||||
_ = try stmt.step();
|
||||
}
|
||||
|
||||
/// Inserts a 3D bounding box into an R-Tree.
|
||||
pub fn insert3D(self: *Self, table_name: []const u8, id: i64, box: BoundingBox3D) !void {
|
||||
const sql = try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"INSERT INTO {s} VALUES (?, ?, ?, ?, ?, ?, ?)\x00",
|
||||
.{table_name},
|
||||
);
|
||||
defer self.allocator.free(sql);
|
||||
|
||||
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindInt(1, id);
|
||||
try stmt.bindFloat(2, box.min_x);
|
||||
try stmt.bindFloat(3, box.max_x);
|
||||
try stmt.bindFloat(4, box.min_y);
|
||||
try stmt.bindFloat(5, box.max_y);
|
||||
try stmt.bindFloat(6, box.min_z);
|
||||
try stmt.bindFloat(7, box.max_z);
|
||||
|
||||
_ = try stmt.step();
|
||||
}
|
||||
|
||||
/// Inserts a point (as a zero-size bounding box) into a 2D R-Tree.
|
||||
pub fn insertPoint2D(self: *Self, table_name: []const u8, id: i64, x: f64, y: f64) !void {
|
||||
try self.insert2D(table_name, id, BoundingBox2D.fromPoint(x, y));
|
||||
}
|
||||
|
||||
/// Inserts a geographic coordinate into a geo R-Tree.
|
||||
pub fn insertGeo(self: *Self, table_name: []const u8, id: i64, coord: GeoCoord) !void {
|
||||
try self.insert2D(table_name, id, .{
|
||||
.min_x = coord.longitude,
|
||||
.max_x = coord.longitude,
|
||||
.min_y = coord.latitude,
|
||||
.max_y = coord.latitude,
|
||||
});
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Update Operations
|
||||
// ========================================================================
|
||||
|
||||
/// Updates a 2D bounding box in an R-Tree.
|
||||
pub fn update2D(self: *Self, table_name: []const u8, id: i64, box: BoundingBox2D) !void {
|
||||
const sql = try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"UPDATE {s} SET min_x=?, max_x=?, min_y=?, max_y=? WHERE id=?\x00",
|
||||
.{table_name},
|
||||
);
|
||||
defer self.allocator.free(sql);
|
||||
|
||||
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindFloat(1, box.min_x);
|
||||
try stmt.bindFloat(2, box.max_x);
|
||||
try stmt.bindFloat(3, box.min_y);
|
||||
try stmt.bindFloat(4, box.max_y);
|
||||
try stmt.bindInt(5, id);
|
||||
|
||||
_ = try stmt.step();
|
||||
}
|
||||
|
||||
/// Deletes an entry from an R-Tree.
|
||||
pub fn delete(self: *Self, table_name: []const u8, id: i64) !void {
|
||||
const sql = try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"DELETE FROM {s} WHERE id=?\x00",
|
||||
.{table_name},
|
||||
);
|
||||
defer self.allocator.free(sql);
|
||||
|
||||
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindInt(1, id);
|
||||
_ = try stmt.step();
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Query Operations
|
||||
// ========================================================================
|
||||
|
||||
/// Queries for all entries that intersect with a bounding box.
|
||||
/// Returns a prepared statement with results: id, min_x, max_x, min_y, max_y
|
||||
pub fn queryIntersects2D(self: *Self, table_name: []const u8, box: BoundingBox2D) !Statement {
|
||||
const sql = try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"SELECT * FROM {s} WHERE min_x <= ? AND max_x >= ? AND min_y <= ? AND max_y >= ?\x00",
|
||||
.{table_name},
|
||||
);
|
||||
defer self.allocator.free(sql);
|
||||
|
||||
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
|
||||
errdefer stmt.finalize();
|
||||
|
||||
try stmt.bindFloat(1, box.max_x);
|
||||
try stmt.bindFloat(2, box.min_x);
|
||||
try stmt.bindFloat(3, box.max_y);
|
||||
try stmt.bindFloat(4, box.min_y);
|
||||
|
||||
return stmt;
|
||||
}
|
||||
|
||||
/// Queries for all entries contained within a bounding box.
|
||||
pub fn queryContained2D(self: *Self, table_name: []const u8, box: BoundingBox2D) !Statement {
|
||||
const sql = try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"SELECT * FROM {s} WHERE min_x >= ? AND max_x <= ? AND min_y >= ? AND max_y <= ?\x00",
|
||||
.{table_name},
|
||||
);
|
||||
defer self.allocator.free(sql);
|
||||
|
||||
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
|
||||
errdefer stmt.finalize();
|
||||
|
||||
try stmt.bindFloat(1, box.min_x);
|
||||
try stmt.bindFloat(2, box.max_x);
|
||||
try stmt.bindFloat(3, box.min_y);
|
||||
try stmt.bindFloat(4, box.max_y);
|
||||
|
||||
return stmt;
|
||||
}
|
||||
|
||||
/// Queries for all entries near a point (within a given radius).
|
||||
pub fn queryNearPoint2D(self: *Self, table_name: []const u8, x: f64, y: f64, radius: f64) !Statement {
|
||||
const box = BoundingBox2D{
|
||||
.min_x = x - radius,
|
||||
.max_x = x + radius,
|
||||
.min_y = y - radius,
|
||||
.max_y = y + radius,
|
||||
};
|
||||
return self.queryIntersects2D(table_name, box);
|
||||
}
|
||||
|
||||
/// Queries for all entries near a geographic coordinate.
|
||||
/// radius is in degrees (approximately: 1 degree ≈ 111km at equator)
|
||||
pub fn queryNearGeo(self: *Self, table_name: []const u8, coord: GeoCoord, radius_degrees: f64) !Statement {
|
||||
return self.queryNearPoint2D(table_name, coord.longitude, coord.latitude, radius_degrees);
|
||||
}
|
||||
|
||||
/// Gets all entry IDs that intersect with a bounding box.
|
||||
pub fn getIntersectingIds2D(self: *Self, table_name: []const u8, box: BoundingBox2D) ![]i64 {
|
||||
var stmt = try self.queryIntersects2D(table_name, box);
|
||||
defer stmt.finalize();
|
||||
|
||||
var ids: std.ArrayListUnmanaged(i64) = .empty;
|
||||
errdefer ids.deinit(self.allocator);
|
||||
|
||||
while (try stmt.step()) {
|
||||
try ids.append(self.allocator, stmt.columnInt(0));
|
||||
}
|
||||
|
||||
return ids.toOwnedSlice(self.allocator);
|
||||
}
|
||||
|
||||
/// Frees an array of IDs returned by getIntersectingIds2D.
|
||||
pub fn freeIds(self: *Self, ids: []i64) void {
|
||||
self.allocator.free(ids);
|
||||
}
|
||||
|
||||
/// Counts entries that intersect with a bounding box.
|
||||
pub fn countIntersects2D(self: *Self, table_name: []const u8, box: BoundingBox2D) !i64 {
|
||||
const sql = try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"SELECT COUNT(*) FROM {s} WHERE min_x <= ? AND max_x >= ? AND min_y <= ? AND max_y >= ?\x00",
|
||||
.{table_name},
|
||||
);
|
||||
defer self.allocator.free(sql);
|
||||
|
||||
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
|
||||
defer stmt.finalize();
|
||||
|
||||
try stmt.bindFloat(1, box.max_x);
|
||||
try stmt.bindFloat(2, box.min_x);
|
||||
try stmt.bindFloat(3, box.max_y);
|
||||
try stmt.bindFloat(4, box.min_y);
|
||||
|
||||
if (try stmt.step()) {
|
||||
return stmt.columnInt(0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Gets the total count of entries in an R-Tree.
|
||||
pub fn count(self: *Self, table_name: []const u8) !i64 {
|
||||
const sql = try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"SELECT COUNT(*) FROM {s}\x00",
|
||||
.{table_name},
|
||||
);
|
||||
defer self.allocator.free(sql);
|
||||
|
||||
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
|
||||
defer stmt.finalize();
|
||||
|
||||
if (try stmt.step()) {
|
||||
return stmt.columnInt(0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Gets the bounding box that encompasses all entries in the R-Tree.
|
||||
pub fn getBounds2D(self: *Self, table_name: []const u8) !?BoundingBox2D {
|
||||
const sql = try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"SELECT MIN(min_x), MAX(max_x), MIN(min_y), MAX(max_y) FROM {s}\x00",
|
||||
.{table_name},
|
||||
);
|
||||
defer self.allocator.free(sql);
|
||||
|
||||
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
|
||||
defer stmt.finalize();
|
||||
|
||||
if (try stmt.step()) {
|
||||
if (stmt.columnIsNull(0)) return null;
|
||||
return BoundingBox2D{
|
||||
.min_x = stmt.columnFloat(0),
|
||||
.max_x = stmt.columnFloat(1),
|
||||
.min_y = stmt.columnFloat(2),
|
||||
.max_y = stmt.columnFloat(3),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Join Operations (R-Tree with regular tables)
|
||||
// ========================================================================
|
||||
|
||||
/// Performs a spatial join between an R-Tree and a regular table.
|
||||
/// Returns entries from both tables where the R-Tree bbox intersects the query box.
|
||||
///
|
||||
/// Example:
|
||||
/// ```zig
|
||||
/// var stmt = try rtree.spatialJoin2D(
|
||||
/// "locations", // R-Tree table
|
||||
/// "places", // Regular table
|
||||
/// "id", // Join column in places
|
||||
/// &.{ "name", "type" }, // Columns to select from places
|
||||
/// query_box,
|
||||
/// );
|
||||
/// ```
|
||||
pub fn spatialJoin2D(
|
||||
self: *Self,
|
||||
rtree_table: []const u8,
|
||||
data_table: []const u8,
|
||||
join_column: []const u8,
|
||||
select_columns: []const []const u8,
|
||||
box: BoundingBox2D,
|
||||
) !Statement {
|
||||
// Build select columns
|
||||
var col_buf: [2048]u8 = undefined;
|
||||
var col_len: usize = 0;
|
||||
|
||||
@memcpy(col_buf[col_len..][0..4], "r.id");
|
||||
col_len += 4;
|
||||
|
||||
for (select_columns) |col| {
|
||||
@memcpy(col_buf[col_len..][0..4], ", d.");
|
||||
col_len += 4;
|
||||
@memcpy(col_buf[col_len..][0..col.len], col);
|
||||
col_len += col.len;
|
||||
}
|
||||
|
||||
const sql = try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"SELECT {s} FROM {s} r JOIN {s} d ON r.id = d.{s} WHERE r.min_x <= ? AND r.max_x >= ? AND r.min_y <= ? AND r.max_y >= ?\x00",
|
||||
.{ col_buf[0..col_len], rtree_table, data_table, join_column },
|
||||
);
|
||||
defer self.allocator.free(sql);
|
||||
|
||||
var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
|
||||
errdefer stmt.finalize();
|
||||
|
||||
try stmt.bindFloat(1, box.max_x);
|
||||
try stmt.bindFloat(2, box.min_x);
|
||||
try stmt.bindFloat(3, box.max_y);
|
||||
try stmt.bindFloat(4, box.min_y);
|
||||
|
||||
return stmt;
|
||||
}
|
||||
};
|
||||
|
|
@ -375,4 +375,317 @@ pub const Statement = struct {
|
|||
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,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
|||
321
src/vtable.zig
Normal file
321
src/vtable.zig
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
//! Virtual Table API
|
||||
//!
|
||||
//! Provides the foundation for creating custom SQLite virtual tables in Zig.
|
||||
//! Virtual tables allow you to expose any data source (files, APIs, computed data)
|
||||
//! as if it were a regular SQL table.
|
||||
//!
|
||||
//! This is an advanced feature - most users should use the higher-level helpers
|
||||
//! like FTS5, JSON, or R-Tree modules instead.
|
||||
|
||||
const std = @import("std");
|
||||
const c = @import("c.zig").c;
|
||||
const Database = @import("database.zig").Database;
|
||||
const errors = @import("errors.zig");
|
||||
|
||||
const Error = errors.Error;
|
||||
const resultToError = errors.resultToError;
|
||||
|
||||
/// Index constraint operators.
|
||||
pub const ConstraintOp = enum(u8) {
|
||||
eq = 2, // SQLITE_INDEX_CONSTRAINT_EQ
|
||||
gt = 4, // SQLITE_INDEX_CONSTRAINT_GT
|
||||
le = 8, // SQLITE_INDEX_CONSTRAINT_LE
|
||||
lt = 16, // SQLITE_INDEX_CONSTRAINT_LT
|
||||
ge = 32, // SQLITE_INDEX_CONSTRAINT_GE
|
||||
match = 64, // SQLITE_INDEX_CONSTRAINT_MATCH
|
||||
like = 65, // SQLITE_INDEX_CONSTRAINT_LIKE
|
||||
glob = 66, // SQLITE_INDEX_CONSTRAINT_GLOB
|
||||
regexp = 67, // SQLITE_INDEX_CONSTRAINT_REGEXP
|
||||
ne = 68, // SQLITE_INDEX_CONSTRAINT_NE
|
||||
isnot = 69, // SQLITE_INDEX_CONSTRAINT_ISNOT
|
||||
isnotnull = 70, // SQLITE_INDEX_CONSTRAINT_ISNOTNULL
|
||||
isnull = 71, // SQLITE_INDEX_CONSTRAINT_ISNULL
|
||||
is = 72, // SQLITE_INDEX_CONSTRAINT_IS
|
||||
limit = 73, // SQLITE_INDEX_CONSTRAINT_LIMIT
|
||||
offset = 74, // SQLITE_INDEX_CONSTRAINT_OFFSET
|
||||
function = 150, // SQLITE_INDEX_CONSTRAINT_FUNCTION
|
||||
};
|
||||
|
||||
/// Constraint information for query planning.
|
||||
pub const IndexConstraint = struct {
|
||||
column: i32,
|
||||
op: ConstraintOp,
|
||||
usable: bool,
|
||||
};
|
||||
|
||||
/// Order by information for query planning.
|
||||
pub const IndexOrderBy = struct {
|
||||
column: i32,
|
||||
desc: bool,
|
||||
};
|
||||
|
||||
/// Index info passed to xBestIndex.
|
||||
pub const IndexInfo = struct {
|
||||
constraints: []IndexConstraint,
|
||||
order_by: []IndexOrderBy,
|
||||
// Output fields set by xBestIndex
|
||||
idx_num: i32 = 0,
|
||||
idx_str: ?[]const u8 = null,
|
||||
order_by_consumed: bool = false,
|
||||
estimated_cost: f64 = 1000000.0,
|
||||
estimated_rows: i64 = 1000000,
|
||||
};
|
||||
|
||||
/// Column type declaration for virtual table schema.
|
||||
pub const ColumnDef = struct {
|
||||
name: []const u8,
|
||||
col_type: []const u8,
|
||||
constraints: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
/// Virtual table module definition.
|
||||
/// Implement this interface to create a custom virtual table.
|
||||
pub fn VTableModule(comptime Context: type, comptime Cursor: type) type {
|
||||
return struct {
|
||||
/// Called when the virtual table is created.
|
||||
/// Should return the column definitions for CREATE TABLE.
|
||||
create_fn: *const fn (db: *Database, args: []const []const u8) anyerror!Context,
|
||||
|
||||
/// Called when the virtual table is connected (opened).
|
||||
connect_fn: *const fn (db: *Database, args: []const []const u8) anyerror!Context,
|
||||
|
||||
/// Called to query the best index for a query.
|
||||
best_index_fn: *const fn (ctx: *Context, info: *IndexInfo) anyerror!void,
|
||||
|
||||
/// Called when the virtual table is destroyed.
|
||||
destroy_fn: *const fn (ctx: *Context) void,
|
||||
|
||||
/// Called to open a cursor for scanning.
|
||||
open_fn: *const fn (ctx: *Context) anyerror!Cursor,
|
||||
|
||||
/// Called to close a cursor.
|
||||
close_fn: *const fn (cursor: *Cursor) void,
|
||||
|
||||
/// Called to start a query with the chosen index.
|
||||
filter_fn: *const fn (cursor: *Cursor, idx_num: i32, idx_str: ?[]const u8, args: []const ?*c.sqlite3_value) anyerror!void,
|
||||
|
||||
/// Called to advance to the next row.
|
||||
next_fn: *const fn (cursor: *Cursor) anyerror!void,
|
||||
|
||||
/// Called to check if we've reached end of data.
|
||||
eof_fn: *const fn (cursor: *Cursor) bool,
|
||||
|
||||
/// Called to get a column value.
|
||||
column_fn: *const fn (cursor: *Cursor, col_idx: i32, result_ctx: *c.sqlite3_context) anyerror!void,
|
||||
|
||||
/// Called to get the rowid.
|
||||
rowid_fn: *const fn (cursor: *Cursor) anyerror!i64,
|
||||
|
||||
/// Optional: Called to update a row.
|
||||
update_fn: ?*const fn (ctx: *Context, argc: i32, argv: [*]?*c.sqlite3_value, rowid: *i64) anyerror!void = null,
|
||||
|
||||
/// Returns the schema for this virtual table.
|
||||
schema_fn: *const fn (ctx: *Context) []const ColumnDef,
|
||||
};
|
||||
}
|
||||
|
||||
/// Simple read-only virtual table helper.
|
||||
/// Easier to use than the full VTableModule for simple cases.
|
||||
pub fn SimpleVTable(comptime Row: type) type {
|
||||
return struct {
|
||||
data: []const Row,
|
||||
columns: []const ColumnDef,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn init(data: []const Row, columns: []const ColumnDef) Self {
|
||||
return .{
|
||||
.data = data,
|
||||
.columns = columns,
|
||||
};
|
||||
}
|
||||
|
||||
/// Cursor for iterating over rows.
|
||||
pub const Cursor = struct {
|
||||
table: *const Self,
|
||||
current_row: usize,
|
||||
|
||||
pub fn next(self: *Cursor) void {
|
||||
self.current_row += 1;
|
||||
}
|
||||
|
||||
pub fn eof(self: *const Cursor) bool {
|
||||
return self.current_row >= self.table.data.len;
|
||||
}
|
||||
|
||||
pub fn currentRow(self: *const Cursor) ?Row {
|
||||
if (self.eof()) return null;
|
||||
return self.table.data[self.current_row];
|
||||
}
|
||||
|
||||
pub fn rowid(self: *const Cursor) i64 {
|
||||
return @intCast(self.current_row + 1);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn openCursor(self: *const Self) Cursor {
|
||||
return .{
|
||||
.table = self,
|
||||
.current_row = 0,
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Generates CREATE TABLE statement for a virtual table schema.
|
||||
pub fn generateSchema(allocator: std.mem.Allocator, table_name: []const u8, columns: []const ColumnDef) ![]u8 {
|
||||
var sql = std.ArrayList(u8).init(allocator);
|
||||
errdefer sql.deinit();
|
||||
|
||||
try sql.appendSlice("CREATE TABLE ");
|
||||
try sql.appendSlice(table_name);
|
||||
try sql.appendSlice("(");
|
||||
|
||||
for (columns, 0..) |col, i| {
|
||||
if (i > 0) try sql.appendSlice(", ");
|
||||
try sql.appendSlice(col.name);
|
||||
try sql.append(' ');
|
||||
try sql.appendSlice(col.col_type);
|
||||
if (col.constraints) |cons| {
|
||||
try sql.append(' ');
|
||||
try sql.appendSlice(cons);
|
||||
}
|
||||
}
|
||||
|
||||
try sql.appendSlice(")");
|
||||
return sql.toOwnedSlice();
|
||||
}
|
||||
|
||||
/// Helper to set a result value in a sqlite3_context.
|
||||
pub const ResultHelper = struct {
|
||||
ctx: *c.sqlite3_context,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn init(ctx: *c.sqlite3_context) Self {
|
||||
return .{ .ctx = ctx };
|
||||
}
|
||||
|
||||
pub fn setNull(self: Self) void {
|
||||
c.sqlite3_result_null(self.ctx);
|
||||
}
|
||||
|
||||
pub fn setInt(self: Self, value: i64) void {
|
||||
c.sqlite3_result_int64(self.ctx, value);
|
||||
}
|
||||
|
||||
pub fn setFloat(self: Self, value: f64) void {
|
||||
c.sqlite3_result_double(self.ctx, value);
|
||||
}
|
||||
|
||||
pub fn setText(self: Self, value: []const u8) void {
|
||||
c.sqlite3_result_text(self.ctx, value.ptr, @intCast(value.len), c.SQLITE_TRANSIENT);
|
||||
}
|
||||
|
||||
pub fn setBlob(self: Self, value: []const u8) void {
|
||||
c.sqlite3_result_blob(self.ctx, value.ptr, @intCast(value.len), c.SQLITE_TRANSIENT);
|
||||
}
|
||||
|
||||
pub fn setError(self: Self, msg: []const u8) void {
|
||||
c.sqlite3_result_error(self.ctx, msg.ptr, @intCast(msg.len));
|
||||
}
|
||||
|
||||
pub fn setErrorCode(self: Self, code: c_int) void {
|
||||
c.sqlite3_result_error_code(self.ctx, code);
|
||||
}
|
||||
};
|
||||
|
||||
/// Helper to read values from sqlite3_value.
|
||||
pub const ValueHelper = struct {
|
||||
value: *c.sqlite3_value,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn init(value: *c.sqlite3_value) Self {
|
||||
return .{ .value = value };
|
||||
}
|
||||
|
||||
pub fn getType(self: Self) c_int {
|
||||
return c.sqlite3_value_type(self.value);
|
||||
}
|
||||
|
||||
pub fn isNull(self: Self) bool {
|
||||
return self.getType() == c.SQLITE_NULL;
|
||||
}
|
||||
|
||||
pub fn getInt(self: Self) i64 {
|
||||
return c.sqlite3_value_int64(self.value);
|
||||
}
|
||||
|
||||
pub fn getFloat(self: Self) f64 {
|
||||
return c.sqlite3_value_double(self.value);
|
||||
}
|
||||
|
||||
pub fn getText(self: Self) ?[]const u8 {
|
||||
const len = c.sqlite3_value_bytes(self.value);
|
||||
const text = c.sqlite3_value_text(self.value);
|
||||
if (text) |t| {
|
||||
return t[0..@intCast(len)];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn getBlob(self: Self) ?[]const u8 {
|
||||
const len = c.sqlite3_value_bytes(self.value);
|
||||
const blob = c.sqlite3_value_blob(self.value);
|
||||
if (blob) |b| {
|
||||
const ptr: [*]const u8 = @ptrCast(b);
|
||||
return ptr[0..@intCast(len)];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/// Eponymous virtual table - appears automatically without CREATE VIRTUAL TABLE.
|
||||
/// Useful for table-valued functions.
|
||||
pub const EponymousModule = struct {
|
||||
name: []const u8,
|
||||
num_args: i32,
|
||||
|
||||
/// Generates SQL to query an eponymous module.
|
||||
pub fn query(self: *const EponymousModule, allocator: std.mem.Allocator, args: []const []const u8) ![]u8 {
|
||||
var sql = std.ArrayList(u8).init(allocator);
|
||||
errdefer sql.deinit();
|
||||
|
||||
try sql.appendSlice("SELECT * FROM ");
|
||||
try sql.appendSlice(self.name);
|
||||
try sql.append('(');
|
||||
|
||||
for (args, 0..) |arg, i| {
|
||||
if (i > 0) try sql.appendSlice(", ");
|
||||
try sql.append('\'');
|
||||
try sql.appendSlice(arg);
|
||||
try sql.append('\'');
|
||||
}
|
||||
|
||||
try sql.append(')');
|
||||
return sql.toOwnedSlice();
|
||||
}
|
||||
};
|
||||
|
||||
// Note: Full virtual table registration requires complex C callback trampolines
|
||||
// similar to what we do for UDFs. The helpers above provide the building blocks
|
||||
// but the actual registration would need additional C interop code.
|
||||
//
|
||||
// For most use cases, the built-in virtual tables (FTS5, R-Tree, JSON) are
|
||||
// sufficient. Custom virtual tables are an advanced feature that few applications
|
||||
// need.
|
||||
|
||||
/// Common virtual table modules available in SQLite.
|
||||
pub const BuiltinModules = struct {
|
||||
/// FTS5 full-text search
|
||||
pub const fts5 = "fts5";
|
||||
/// R-Tree spatial index
|
||||
pub const rtree = "rtree";
|
||||
/// CSV virtual table (if compiled with)
|
||||
pub const csv = "csv";
|
||||
/// Generate series (built-in table-valued function)
|
||||
pub const generate_series = "generate_series";
|
||||
/// PRAGMA function as table
|
||||
pub const pragma = "pragma";
|
||||
};
|
||||
Loading…
Reference in a new issue