mirror of
https://github.com/sbrow/envr.git
synced 2026-06-27 10:38:33 -04:00
1332 lines
47 KiB
Zig
1332 lines
47 KiB
Zig
const std = @import("std");
|
|
const debug = std.debug;
|
|
const fmt = std.fmt;
|
|
const heap = std.heap;
|
|
const mem = std.mem;
|
|
const meta = std.meta;
|
|
const testing = std.testing;
|
|
|
|
const c = @import("c.zig").c;
|
|
const versionGreaterThanOrEqualTo = @import("c.zig").versionGreaterThanOrEqualTo;
|
|
const getTestDb = @import("test.zig").getTestDb;
|
|
const Diagnostics = @import("sqlite.zig").Diagnostics;
|
|
const Blob = @import("sqlite.zig").Blob;
|
|
const Text = @import("sqlite.zig").Text;
|
|
const helpers = @import("helpers.zig");
|
|
|
|
const logger = std.log.scoped(.vtab);
|
|
|
|
fn hasDecls(comptime T: type, comptime names: anytype) bool {
|
|
inline for (names) |name| {
|
|
if (!@hasDecl(T, name)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// ModuleContext contains state that is needed by all implementations of virtual tables.
|
|
///
|
|
/// Currently there's only an allocator.
|
|
pub const ModuleContext = struct {
|
|
allocator: mem.Allocator,
|
|
};
|
|
|
|
fn dupeToSQLiteString(s: []const u8) [*c]u8 {
|
|
var buffer: [*c]u8 = @ptrCast(c.sqlite3_malloc(@intCast(s.len + 1)));
|
|
|
|
mem.copyForwards(u8, buffer[0..s.len], s);
|
|
buffer[s.len] = 0;
|
|
|
|
return buffer;
|
|
}
|
|
|
|
/// VTabDiagnostics is used by the user to report error diagnostics to the virtual table.
|
|
pub const VTabDiagnostics = struct {
|
|
const Self = @This();
|
|
|
|
allocator: mem.Allocator,
|
|
|
|
error_message: []const u8 = "unknown error",
|
|
|
|
pub fn setErrorMessage(self: *Self, comptime format_string: []const u8, values: anytype) void {
|
|
self.error_message = fmt.allocPrintSentinel(self.allocator, format_string, values, 0) catch |err| switch (err) {
|
|
error.OutOfMemory => "can't set diagnostic message, out of memory",
|
|
};
|
|
}
|
|
};
|
|
|
|
pub const BestIndexBuilder = struct {
|
|
const Self = @This();
|
|
|
|
/// Constraint operator codes.
|
|
/// See https://sqlite.org/c3ref/c_index_constraint_eq.html
|
|
pub const ConstraintOp = if (versionGreaterThanOrEqualTo(3, 38, 0))
|
|
enum {
|
|
eq,
|
|
gt,
|
|
le,
|
|
lt,
|
|
ge,
|
|
match,
|
|
like,
|
|
glob,
|
|
regexp,
|
|
ne,
|
|
is_not,
|
|
is_not_null,
|
|
is_null,
|
|
is,
|
|
limit,
|
|
offset,
|
|
}
|
|
else
|
|
enum {
|
|
eq,
|
|
gt,
|
|
le,
|
|
lt,
|
|
ge,
|
|
match,
|
|
like,
|
|
glob,
|
|
regexp,
|
|
ne,
|
|
is_not,
|
|
is_not_null,
|
|
is_null,
|
|
is,
|
|
};
|
|
|
|
const ConstraintOpFromCodeError = error{
|
|
InvalidCode,
|
|
};
|
|
|
|
fn constraintOpFromCode(code: u8) ConstraintOpFromCodeError!ConstraintOp {
|
|
if (comptime versionGreaterThanOrEqualTo(3, 38, 0)) {
|
|
switch (code) {
|
|
c.SQLITE_INDEX_CONSTRAINT_LIMIT => return .limit,
|
|
c.SQLITE_INDEX_CONSTRAINT_OFFSET => return .offset,
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
switch (code) {
|
|
c.SQLITE_INDEX_CONSTRAINT_EQ => return .eq,
|
|
c.SQLITE_INDEX_CONSTRAINT_GT => return .gt,
|
|
c.SQLITE_INDEX_CONSTRAINT_LE => return .le,
|
|
c.SQLITE_INDEX_CONSTRAINT_LT => return .lt,
|
|
c.SQLITE_INDEX_CONSTRAINT_GE => return .ge,
|
|
c.SQLITE_INDEX_CONSTRAINT_MATCH => return .match,
|
|
c.SQLITE_INDEX_CONSTRAINT_LIKE => return .like,
|
|
c.SQLITE_INDEX_CONSTRAINT_GLOB => return .glob,
|
|
c.SQLITE_INDEX_CONSTRAINT_REGEXP => return .regexp,
|
|
c.SQLITE_INDEX_CONSTRAINT_NE => return .ne,
|
|
c.SQLITE_INDEX_CONSTRAINT_ISNOT => return .is_not,
|
|
c.SQLITE_INDEX_CONSTRAINT_ISNOTNULL => return .is_not_null,
|
|
c.SQLITE_INDEX_CONSTRAINT_ISNULL => return .is_null,
|
|
c.SQLITE_INDEX_CONSTRAINT_IS => return .is,
|
|
else => return error.InvalidCode,
|
|
}
|
|
}
|
|
|
|
// WHERE clause constraint
|
|
pub const Constraint = struct {
|
|
// Column constrained. -1 for ROWID
|
|
column: isize,
|
|
op: ConstraintOp,
|
|
usable: bool,
|
|
|
|
usage: struct {
|
|
// If >0, constraint is part of argv to xFilter
|
|
argv_index: i32 = 0,
|
|
// Id >0, do not code a test for this constraint
|
|
omit: bool = false,
|
|
},
|
|
};
|
|
|
|
// ORDER BY clause
|
|
pub const OrderBy = struct {
|
|
column: usize,
|
|
order: enum {
|
|
desc,
|
|
asc,
|
|
},
|
|
};
|
|
|
|
/// Internal state
|
|
allocator: mem.Allocator,
|
|
id_str_buffer: std.ArrayList(u8),
|
|
index_info: *c.sqlite3_index_info,
|
|
|
|
/// List of WHERE clause constraints
|
|
///
|
|
/// Similar to `aConstraint` in the Inputs section of sqlite3_index_info except we embed the constraint usage in there too.
|
|
/// This makes it nicer to use for the user.
|
|
constraints: []Constraint,
|
|
|
|
/// Indicate which columns of the virtual table are actually used by the statement.
|
|
/// If the lowest bit of colUsed is set, that means that the first column is used.
|
|
/// The second lowest bit corresponds to the second column. And so forth.
|
|
///
|
|
/// Maps to the `colUsed` field.
|
|
columns_used: u64,
|
|
|
|
/// Index identifier.
|
|
/// This is passed to the filtering function to identify which index to use.
|
|
///
|
|
/// Maps to the `idxNum` and `idxStr` field in sqlite3_index_info.
|
|
/// Id id.id_str is non empty the string will be copied to a SQLite-allocated buffer and `needToFreeIdxStr` will be 1.
|
|
id: IndexIdentifier,
|
|
|
|
/// If the virtual table will output its rows already in the order specified by the ORDER BY clause then this can be set to true.
|
|
/// This will indicate to SQLite that it doesn't need to do a sorting pass.
|
|
///
|
|
/// Maps to the `orderByConsumed` field.
|
|
already_ordered: bool = false,
|
|
|
|
/// Estimated number of "disk access operations" required to execute this query.
|
|
///
|
|
/// Maps to the `estimatedCost` field.
|
|
estimated_cost: ?f64 = null,
|
|
|
|
/// Estimated number of rows returned by this query.
|
|
///
|
|
/// Maps to the `estimatedRows` field.
|
|
///
|
|
/// ODO(vincent): implement this
|
|
estimated_rows: ?i64 = null,
|
|
|
|
/// Additiounal flags for this index.
|
|
///
|
|
/// Maps to the `idxFlags` field.
|
|
flags: struct {
|
|
unique: bool = false,
|
|
} = .{},
|
|
|
|
const InitError = error{} || mem.Allocator.Error || ConstraintOpFromCodeError;
|
|
|
|
fn init(allocator: mem.Allocator, index_info: *c.sqlite3_index_info) InitError!Self {
|
|
const res = Self{
|
|
.allocator = allocator,
|
|
.index_info = index_info,
|
|
.id_str_buffer = .empty,
|
|
.constraints = try allocator.alloc(Constraint, @intCast(index_info.nConstraint)),
|
|
.columns_used = @intCast(index_info.colUsed),
|
|
.id = .{},
|
|
};
|
|
|
|
for (res.constraints, 0..) |*constraint, i| {
|
|
const raw_constraint = index_info.aConstraint[i];
|
|
|
|
constraint.column = @intCast(raw_constraint.iColumn);
|
|
constraint.op = try constraintOpFromCode(raw_constraint.op);
|
|
constraint.usable = if (raw_constraint.usable == 1) true else false;
|
|
constraint.usage = .{};
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
/// Returns true if the column is used, false otherwise.
|
|
pub fn isColumnUsed(self: *Self, column: u6) bool {
|
|
const mask = @as(u64, 1) << column - 1;
|
|
return self.columns_used & mask == mask;
|
|
}
|
|
|
|
/// Builds the final index data.
|
|
///
|
|
/// Internally it populates the sqlite3_index_info "Outputs" fields using the information set by the user.
|
|
pub fn build(self: *Self) void {
|
|
var index_info = self.index_info;
|
|
|
|
// Populate the constraint usage
|
|
var constraint_usage = index_info.aConstraintUsage[0..self.constraints.len];
|
|
for (self.constraints, 0..) |constraint, i| {
|
|
constraint_usage[i].argvIndex = constraint.usage.argv_index;
|
|
constraint_usage[i].omit = if (constraint.usage.omit) 1 else 0;
|
|
}
|
|
|
|
// Identifiers
|
|
index_info.idxNum = @intCast(self.id.num);
|
|
if (self.id.str.len > 0) {
|
|
// Must always be NULL-terminated so add 1
|
|
const tmp: [*c]u8 = @ptrCast(c.sqlite3_malloc(@intCast(self.id.str.len + 1)));
|
|
|
|
mem.copyForwards(u8, tmp[0..self.id.str.len], self.id.str);
|
|
tmp[self.id.str.len] = 0;
|
|
|
|
index_info.idxStr = tmp;
|
|
index_info.needToFreeIdxStr = 1;
|
|
}
|
|
|
|
index_info.orderByConsumed = if (self.already_ordered) 1 else 0;
|
|
if (self.estimated_cost) |estimated_cost| {
|
|
index_info.estimatedCost = estimated_cost;
|
|
}
|
|
if (self.estimated_rows) |estimated_rows| {
|
|
index_info.estimatedRows = estimated_rows;
|
|
}
|
|
|
|
// Flags
|
|
index_info.idxFlags = 0;
|
|
if (self.flags.unique) {
|
|
index_info.idxFlags |= c.SQLITE_INDEX_SCAN_UNIQUE;
|
|
}
|
|
}
|
|
};
|
|
|
|
/// Identifies an index for a virtual table.
|
|
///
|
|
/// The user-provided buildBestIndex functions sets the index identifier.
|
|
/// These fields are meaningless for SQLite so they can be whatever you want as long as
|
|
/// both buildBestIndex and filter functions agree on what they mean.
|
|
pub const IndexIdentifier = struct {
|
|
num: i32 = 0,
|
|
str: []const u8 = "",
|
|
|
|
fn fromC(idx_num: c_int, idx_str: [*c]const u8) IndexIdentifier {
|
|
return IndexIdentifier{
|
|
.num = @intCast(idx_num),
|
|
.str = if (idx_str != null) mem.sliceTo(idx_str, 0) else "",
|
|
};
|
|
}
|
|
};
|
|
|
|
pub const FilterArg = struct {
|
|
value: ?*c.sqlite3_value,
|
|
|
|
pub fn as(self: FilterArg, comptime Type: type) Type {
|
|
var result: Type = undefined;
|
|
helpers.setTypeFromValue(Type, &result, self.value.?);
|
|
|
|
return result;
|
|
}
|
|
};
|
|
|
|
/// Validates that a type implements everything required to be a cursor for a virtual table.
|
|
fn validateCursorType(comptime Table: type) void {
|
|
const Cursor = Table.Cursor;
|
|
|
|
// Validate the `init` function
|
|
{
|
|
if (!comptime hasDecls(Cursor, .{"InitError"})) {
|
|
@compileError("the Cursor type must declare a InitError error set for the init function");
|
|
}
|
|
|
|
const error_message =
|
|
\\the Cursor.init function must have the signature `fn init(allocator: std.mem.Allocator, parent: *Table) InitError!*Cursor`
|
|
;
|
|
|
|
if (!comptime helpers.hasFn(Cursor, "init")) {
|
|
@compileError("the Cursor type must have an init function, " ++ error_message);
|
|
}
|
|
|
|
const info = @typeInfo(@TypeOf(Cursor.init)).@"fn";
|
|
|
|
if (info.params.len != 2) @compileError(error_message);
|
|
if (info.params[0].type.? != mem.Allocator) @compileError(error_message);
|
|
if (info.params[1].type.? != *Table) @compileError(error_message);
|
|
if (info.return_type.? != Cursor.InitError!*Cursor) @compileError(error_message);
|
|
}
|
|
|
|
// Validate the `deinit` function
|
|
{
|
|
const error_message =
|
|
\\the Cursor.deinit function must have the signature `fn deinit(cursor: *Cursor) void`
|
|
;
|
|
|
|
if (!comptime helpers.hasFn(Cursor, "deinit")) {
|
|
@compileError("the Cursor type must have a deinit function, " ++ error_message);
|
|
}
|
|
|
|
const info = @typeInfo(@TypeOf(Cursor.deinit)).@"fn";
|
|
|
|
if (info.params.len != 1) @compileError(error_message);
|
|
if (info.params[0].type.? != *Cursor) @compileError(error_message);
|
|
if (info.return_type.? != void) @compileError(error_message);
|
|
}
|
|
|
|
// Validate the `next` function
|
|
{
|
|
if (!comptime hasDecls(Cursor, .{"NextError"})) {
|
|
@compileError("the Cursor type must declare a NextError error set for the next function");
|
|
}
|
|
|
|
const error_message =
|
|
\\the Cursor.next function must have the signature `fn next(cursor: *Cursor, diags: *sqlite.vtab.VTabDiagnostics) NextError!void`
|
|
;
|
|
|
|
if (!comptime helpers.hasFn(Cursor, "next")) {
|
|
@compileError("the Cursor type must have a next function, " ++ error_message);
|
|
}
|
|
|
|
const info = @typeInfo(@TypeOf(Cursor.next)).@"fn";
|
|
|
|
if (info.params.len != 2) @compileError(error_message);
|
|
if (info.params[0].type.? != *Cursor) @compileError(error_message);
|
|
if (info.params[1].type.? != *VTabDiagnostics) @compileError(error_message);
|
|
if (info.return_type.? != Cursor.NextError!void) @compileError(error_message);
|
|
}
|
|
|
|
// Validate the `hasNext` function
|
|
{
|
|
if (!comptime hasDecls(Cursor, .{"HasNextError"})) {
|
|
@compileError("the Cursor type must declare a HasNextError error set for the hasNext function");
|
|
}
|
|
|
|
const error_message =
|
|
\\the Cursor.hasNext function must have the signature `fn hasNext(cursor: *Cursor, diags: *sqlite.vtab.VTabDiagnostics) HasNextError!bool`
|
|
;
|
|
|
|
if (!comptime helpers.hasFn(Cursor, "hasNext")) {
|
|
@compileError("the Cursor type must have a hasNext function, " ++ error_message);
|
|
}
|
|
|
|
const info = @typeInfo(@TypeOf(Cursor.hasNext)).@"fn";
|
|
|
|
if (info.params.len != 2) @compileError(error_message);
|
|
if (info.params[0].type.? != *Cursor) @compileError(error_message);
|
|
if (info.params[1].type.? != *VTabDiagnostics) @compileError(error_message);
|
|
if (info.return_type.? != Cursor.HasNextError!bool) @compileError(error_message);
|
|
}
|
|
|
|
// Validate the `filter` function
|
|
{
|
|
if (!comptime hasDecls(Cursor, .{"FilterError"})) {
|
|
@compileError("the Cursor type must declare a FilterError error set for the filter function");
|
|
}
|
|
|
|
const error_message =
|
|
\\the Cursor.filter function must have the signature `fn filter(cursor: *Cursor, diags: *sqlite.vtab.VTabDiagnostics, index: sqlite.vtab.IndexIdentifier, args: []FilterArg) FilterError!bool`
|
|
;
|
|
|
|
if (!comptime helpers.hasFn(Cursor, "filter")) {
|
|
@compileError("the Cursor type must have a filter function, " ++ error_message);
|
|
}
|
|
|
|
const info = @typeInfo(@TypeOf(Cursor.filter)).@"fn";
|
|
|
|
if (info.params.len != 4) @compileError(error_message);
|
|
if (info.params[0].type.? != *Cursor) @compileError(error_message);
|
|
if (info.params[1].type.? != *VTabDiagnostics) @compileError(error_message);
|
|
if (info.params[2].type.? != IndexIdentifier) @compileError(error_message);
|
|
if (info.params[3].type.? != []FilterArg) @compileError(error_message);
|
|
if (info.return_type.? != Cursor.FilterError!void) @compileError(error_message);
|
|
}
|
|
|
|
// Validate the `column` function
|
|
{
|
|
if (!comptime hasDecls(Cursor, .{"ColumnError"})) {
|
|
@compileError("the Cursor type must declare a ColumnError error set for the column function");
|
|
}
|
|
if (!comptime hasDecls(Cursor, .{"Column"})) {
|
|
@compileError("the Cursor type must declare a Column type for the return type of the column function");
|
|
}
|
|
|
|
const error_message =
|
|
\\the Cursor.column function must have the signature `fn column(cursor: *Cursor, diags: *sqlite.vtab.VTabDiagnostics, column_number: i32) ColumnError!Column`
|
|
;
|
|
|
|
if (!comptime helpers.hasFn(Cursor, "column")) {
|
|
@compileError("the Cursor type must have a column function, " ++ error_message);
|
|
}
|
|
|
|
const info = @typeInfo(@TypeOf(Cursor.column)).@"fn";
|
|
|
|
if (info.params.len != 3) @compileError(error_message);
|
|
if (info.params[0].type.? != *Cursor) @compileError(error_message);
|
|
if (info.params[1].type.? != *VTabDiagnostics) @compileError(error_message);
|
|
if (info.params[2].type.? != i32) @compileError(error_message);
|
|
if (info.return_type.? != Cursor.ColumnError!Cursor.Column) @compileError(error_message);
|
|
}
|
|
|
|
// Validate the `rowId` function
|
|
{
|
|
if (!comptime hasDecls(Cursor, .{"RowIDError"})) {
|
|
@compileError("the Cursor type must declare a RowIDError error set for the rowId function");
|
|
}
|
|
|
|
const error_message =
|
|
\\the Cursor.rowId function must have the signature `fn rowId(cursor: *Cursor, diags: *sqlite.vtab.VTabDiagnostics) RowIDError!i64`
|
|
;
|
|
|
|
if (!comptime helpers.hasFn(Cursor, "rowId")) {
|
|
@compileError("the Cursor type must have a rowId function, " ++ error_message);
|
|
}
|
|
|
|
const info = @typeInfo(@TypeOf(Cursor.rowId)).@"fn";
|
|
|
|
if (info.params.len != 2) @compileError(error_message);
|
|
if (info.params[0].type.? != *Cursor) @compileError(error_message);
|
|
if (info.params[1].type.? != *VTabDiagnostics) @compileError(error_message);
|
|
if (info.return_type.? != Cursor.RowIDError!i64) @compileError(error_message);
|
|
}
|
|
}
|
|
|
|
/// Validates that a type implements everything required to be a virtual table.
|
|
fn validateTableType(comptime Table: type) void {
|
|
// Validate the `init` function
|
|
{
|
|
if (!comptime hasDecls(Table, .{"InitError"})) {
|
|
@compileError("the Table type must declare a InitError error set for the init function");
|
|
}
|
|
|
|
const error_message =
|
|
\\the Table.init function must have the signature `fn init(allocator: std.mem.Allocator, diags: *sqlite.vtab.VTabDiagnostics, args: []const ModuleArgument) InitError!*Table`
|
|
;
|
|
|
|
if (!comptime helpers.hasFn(Table, "init")) {
|
|
@compileError("the Table type must have a init function, " ++ error_message);
|
|
}
|
|
|
|
const info = @typeInfo(@TypeOf(Table.init)).@"fn";
|
|
|
|
if (info.params.len != 3) @compileError(error_message);
|
|
if (info.params[0].type.? != mem.Allocator) @compileError(error_message);
|
|
if (info.params[1].type.? != *VTabDiagnostics) @compileError(error_message);
|
|
// TODO(vincent): maybe allow a signature without the params since a table can do withoout them
|
|
if (info.params[2].type.? != []const ModuleArgument) @compileError(error_message);
|
|
if (info.return_type.? != Table.InitError!*Table) @compileError(error_message);
|
|
}
|
|
|
|
// Validate the `deinit` function
|
|
{
|
|
const error_message =
|
|
\\the Table.deinit function must have the signature `fn deinit(table: *Table, allocator: std.mem.Allocator) void`
|
|
;
|
|
|
|
if (!comptime helpers.hasFn(Table, "deinit")) {
|
|
@compileError("the Table type must have a deinit function, " ++ error_message);
|
|
}
|
|
|
|
const info = @typeInfo(@TypeOf(Table.deinit)).@"fn";
|
|
|
|
if (info.params.len != 2) @compileError(error_message);
|
|
if (info.params[0].type.? != *Table) @compileError(error_message);
|
|
if (info.params[1].type.? != mem.Allocator) @compileError(error_message);
|
|
if (info.return_type.? != void) @compileError(error_message);
|
|
}
|
|
|
|
// Validate the `buildBestIndex` function
|
|
{
|
|
if (!comptime hasDecls(Table, .{"BuildBestIndexError"})) {
|
|
@compileError("the Cursor type must declare a BuildBestIndexError error set for the buildBestIndex function");
|
|
}
|
|
|
|
const error_message =
|
|
\\the Table.buildBestIndex function must have the signature `fn buildBestIndex(table: *Table, diags: *sqlite.vtab.VTabDiagnostics, builder: *sqlite.vtab.BestIndexBuilder) BuildBestIndexError!void`
|
|
;
|
|
|
|
if (!comptime helpers.hasFn(Table, "buildBestIndex")) {
|
|
@compileError("the Table type must have a buildBestIndex function, " ++ error_message);
|
|
}
|
|
|
|
const info = @typeInfo(@TypeOf(Table.buildBestIndex)).@"fn";
|
|
|
|
if (info.params.len != 3) @compileError(error_message);
|
|
if (info.params[0].type.? != *Table) @compileError(error_message);
|
|
if (info.params[1].type.? != *VTabDiagnostics) @compileError(error_message);
|
|
if (info.params[2].type.? != *BestIndexBuilder) @compileError(error_message);
|
|
if (info.return_type.? != Table.BuildBestIndexError!void) @compileError(error_message);
|
|
}
|
|
|
|
if (!comptime hasDecls(Table, .{"Cursor"})) {
|
|
@compileError("the Table type must declare a Cursor type");
|
|
}
|
|
}
|
|
|
|
pub const ModuleArgument = union(enum) {
|
|
kv: struct {
|
|
key: []const u8,
|
|
value: []const u8,
|
|
},
|
|
plain: []const u8,
|
|
};
|
|
|
|
const ParseModuleArgumentsError = error{} || mem.Allocator.Error;
|
|
|
|
fn parseModuleArguments(allocator: mem.Allocator, argc: c_int, argv: [*c]const [*c]const u8) ParseModuleArgumentsError![]ModuleArgument {
|
|
const res = try allocator.alloc(ModuleArgument, @intCast(argc));
|
|
errdefer allocator.free(res);
|
|
|
|
for (res, 0..) |*marg, i| {
|
|
// The documentation of sqlite says each string in argv is null-terminated
|
|
const arg = mem.sliceTo(argv[i], 0);
|
|
|
|
if (mem.indexOfScalar(u8, arg, '=')) |pos| {
|
|
marg.* = ModuleArgument{
|
|
.kv = .{
|
|
.key = arg[0..pos],
|
|
.value = arg[pos + 1 ..],
|
|
},
|
|
};
|
|
} else {
|
|
marg.* = ModuleArgument{ .plain = arg };
|
|
}
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
pub fn VirtualTable(
|
|
comptime table_name: [:0]const u8,
|
|
comptime Table: type,
|
|
) type {
|
|
// Validate the Table type
|
|
|
|
comptime {
|
|
validateTableType(Table);
|
|
validateCursorType(Table);
|
|
}
|
|
|
|
const State = struct {
|
|
const Self = @This();
|
|
|
|
/// vtab must come first !
|
|
/// The different functions receive a pointer to a vtab so we have to use @fieldParentPtr to get our state.
|
|
vtab: c.sqlite3_vtab,
|
|
/// The module context contains state that's the same for _all_ implementations of virtual tables.
|
|
module_context: *ModuleContext,
|
|
/// The table is the actual virtual table implementation.
|
|
table: *Table,
|
|
|
|
const InitError = error{} || mem.Allocator.Error || Table.InitError;
|
|
|
|
fn init(module_context: *ModuleContext, table: *Table) InitError!*Self {
|
|
const res = try module_context.allocator.create(Self);
|
|
res.* = .{
|
|
.vtab = mem.zeroes(c.sqlite3_vtab),
|
|
.module_context = module_context,
|
|
.table = table,
|
|
};
|
|
return res;
|
|
}
|
|
|
|
fn deinit(self: *Self) void {
|
|
self.table.deinit(self.module_context.allocator);
|
|
self.module_context.allocator.destroy(self);
|
|
}
|
|
};
|
|
|
|
const CursorState = struct {
|
|
const Self = @This();
|
|
|
|
/// vtab_cursor must come first !
|
|
/// The different functions receive a pointer to a vtab_cursor so we have to use @fieldParentPtr to get our state.
|
|
vtab_cursor: c.sqlite3_vtab_cursor,
|
|
/// The module context contains state that's the same for _all_ implementations of virtual tables.
|
|
module_context: *ModuleContext,
|
|
/// The table is the actual virtual table implementation.
|
|
table: *Table,
|
|
cursor: *Table.Cursor,
|
|
|
|
const InitError = error{} || mem.Allocator.Error || Table.Cursor.InitError;
|
|
|
|
fn init(module_context: *ModuleContext, table: *Table) InitError!*Self {
|
|
const res = try module_context.allocator.create(Self);
|
|
errdefer module_context.allocator.destroy(res);
|
|
|
|
res.* = .{
|
|
.vtab_cursor = mem.zeroes(c.sqlite3_vtab_cursor),
|
|
.module_context = module_context,
|
|
.table = table,
|
|
.cursor = try Table.Cursor.init(module_context.allocator, table),
|
|
};
|
|
|
|
return res;
|
|
}
|
|
|
|
fn deinit(self: *Self) void {
|
|
self.cursor.deinit();
|
|
self.module_context.allocator.destroy(self);
|
|
}
|
|
};
|
|
|
|
return struct {
|
|
const Self = @This();
|
|
|
|
pub const name = table_name;
|
|
pub const module = if (versionGreaterThanOrEqualTo(3, 26, 0))
|
|
c.sqlite3_module{
|
|
.iVersion = 0,
|
|
.xCreate = xConnect, // TODO(vincent): implement xCreate and use it
|
|
.xConnect = xConnect,
|
|
.xBestIndex = xBestIndex,
|
|
.xDisconnect = xDisconnect,
|
|
.xDestroy = xDisconnect, // TODO(vincent): implement xDestroy and use it
|
|
.xOpen = xOpen,
|
|
.xClose = xClose,
|
|
.xFilter = xFilter,
|
|
.xNext = xNext,
|
|
.xEof = xEof,
|
|
.xColumn = xColumn,
|
|
.xRowid = xRowid,
|
|
.xUpdate = null,
|
|
.xBegin = null,
|
|
.xSync = null,
|
|
.xCommit = null,
|
|
.xRollback = null,
|
|
.xFindFunction = null,
|
|
.xRename = null,
|
|
.xSavepoint = null,
|
|
.xRelease = null,
|
|
.xRollbackTo = null,
|
|
.xShadowName = null,
|
|
}
|
|
else
|
|
c.sqlite3_module{
|
|
.iVersion = 0,
|
|
.xCreate = xConnect, // TODO(vincent): implement xCreate and use it
|
|
.xConnect = xConnect,
|
|
.xBestIndex = xBestIndex,
|
|
.xDisconnect = xDisconnect,
|
|
.xDestroy = xDisconnect, // TODO(vincent): implement xDestroy and use it
|
|
.xOpen = xOpen,
|
|
.xClose = xClose,
|
|
.xFilter = xFilter,
|
|
.xNext = xNext,
|
|
.xEof = xEof,
|
|
.xColumn = xColumn,
|
|
.xRowid = xRowid,
|
|
.xUpdate = null,
|
|
.xBegin = null,
|
|
.xSync = null,
|
|
.xCommit = null,
|
|
.xRollback = null,
|
|
.xFindFunction = null,
|
|
.xRename = null,
|
|
.xSavepoint = null,
|
|
.xRelease = null,
|
|
.xRollbackTo = null,
|
|
};
|
|
|
|
table: Table,
|
|
|
|
fn getModuleContext(ptr: ?*anyopaque) *ModuleContext {
|
|
return @ptrCast(@alignCast(ptr.?));
|
|
}
|
|
|
|
fn createState(allocator: mem.Allocator, diags: *VTabDiagnostics, module_context: *ModuleContext, args: []const ModuleArgument) !*State {
|
|
// The Context holds the complete of the virtual table and lives for its entire lifetime.
|
|
// Context.deinit() will be called when xDestroy is called.
|
|
|
|
var table = try Table.init(allocator, diags, args);
|
|
errdefer table.deinit(allocator);
|
|
|
|
return try State.init(module_context, table);
|
|
}
|
|
|
|
fn xCreate(db: ?*c.sqlite3, module_context_ptr: ?*anyopaque, argc: c_int, argv: [*c]const [*c]const u8, vtab: [*c][*c]c.sqlite3_vtab, err_str: [*c][*c]const u8) callconv(.c) c_int {
|
|
_ = db;
|
|
_ = module_context_ptr;
|
|
_ = argc;
|
|
_ = argv;
|
|
_ = vtab;
|
|
_ = err_str;
|
|
|
|
debug.print("xCreate\n", .{});
|
|
|
|
return c.SQLITE_ERROR;
|
|
}
|
|
|
|
fn xConnect(db: ?*c.sqlite3, module_context_ptr: ?*anyopaque, argc: c_int, argv: [*c]const [*c]const u8, vtab: [*c][*c]c.sqlite3_vtab, err_str: [*c][*c]u8) callconv(.c) c_int {
|
|
const module_context = getModuleContext(module_context_ptr);
|
|
|
|
var arena = heap.ArenaAllocator.init(module_context.allocator);
|
|
defer arena.deinit();
|
|
|
|
// Convert the C-like args to more idiomatic types.
|
|
const args = parseModuleArguments(arena.allocator(), argc, argv) catch {
|
|
err_str.* = dupeToSQLiteString("out of memory");
|
|
return c.SQLITE_ERROR;
|
|
};
|
|
|
|
//
|
|
// Create the context and state, assign it to the vtab and declare the vtab.
|
|
//
|
|
|
|
var diags = VTabDiagnostics{ .allocator = arena.allocator() };
|
|
const state = createState(module_context.allocator, &diags, module_context, args) catch {
|
|
err_str.* = dupeToSQLiteString(diags.error_message);
|
|
return c.SQLITE_ERROR;
|
|
};
|
|
vtab.* = @ptrCast(state);
|
|
|
|
const res = c.sqlite3_declare_vtab(db, @ptrCast(state.table.schema));
|
|
if (res != c.SQLITE_OK) {
|
|
return c.SQLITE_ERROR;
|
|
}
|
|
|
|
return c.SQLITE_OK;
|
|
}
|
|
|
|
fn xBestIndex(vtab: [*c]c.sqlite3_vtab, index_info_ptr: [*c]c.sqlite3_index_info) callconv(.c) c_int {
|
|
const index_info: *c.sqlite3_index_info = index_info_ptr orelse unreachable;
|
|
|
|
//
|
|
|
|
const vtab_ptr: *c.sqlite3_vtab = @ptrCast(vtab);
|
|
const nullable_state: ?*State = @fieldParentPtr("vtab", vtab_ptr);
|
|
const state = nullable_state orelse unreachable;
|
|
|
|
var arena = heap.ArenaAllocator.init(state.module_context.allocator);
|
|
defer arena.deinit();
|
|
|
|
// Create an index builder and let the user build the index.
|
|
|
|
var builder = BestIndexBuilder.init(arena.allocator(), index_info) catch |err| {
|
|
logger.err("unable to create best index builder, err: {}", .{err});
|
|
return c.SQLITE_ERROR;
|
|
};
|
|
|
|
var diags = VTabDiagnostics{ .allocator = arena.allocator() };
|
|
state.table.buildBestIndex(&diags, &builder) catch |err| {
|
|
logger.err("unable to build best index, err: {}", .{err});
|
|
return c.SQLITE_ERROR;
|
|
};
|
|
|
|
return c.SQLITE_OK;
|
|
}
|
|
|
|
fn xDisconnect(vtab: [*c]c.sqlite3_vtab) callconv(.c) c_int {
|
|
const vtab_ptr: *c.sqlite3_vtab = @ptrCast(vtab);
|
|
const nullable_state: ?*State = @fieldParentPtr("vtab", vtab_ptr);
|
|
const state = nullable_state orelse unreachable;
|
|
|
|
state.deinit();
|
|
|
|
return c.SQLITE_OK;
|
|
}
|
|
|
|
fn xDestroy(vtab: [*c]c.sqlite3_vtab) callconv(.c) c_int {
|
|
_ = vtab;
|
|
|
|
debug.print("xDestroy\n", .{});
|
|
|
|
return c.SQLITE_ERROR;
|
|
}
|
|
|
|
fn xOpen(vtab: [*c]c.sqlite3_vtab, vtab_cursor: [*c][*c]c.sqlite3_vtab_cursor) callconv(.c) c_int {
|
|
const vtab_ptr: *c.sqlite3_vtab = @ptrCast(vtab);
|
|
const nullable_state: ?*State = @fieldParentPtr("vtab", vtab_ptr);
|
|
const state = nullable_state orelse unreachable;
|
|
|
|
const cursor_state = CursorState.init(state.module_context, state.table) catch |err| {
|
|
logger.err("unable to create cursor state, err: {}", .{err});
|
|
return c.SQLITE_ERROR;
|
|
};
|
|
vtab_cursor.* = @ptrCast(cursor_state);
|
|
|
|
return c.SQLITE_OK;
|
|
}
|
|
|
|
fn xClose(vtab_cursor: [*c]c.sqlite3_vtab_cursor) callconv(.c) c_int {
|
|
const vtab_cursor_ptr: *c.sqlite3_vtab_cursor = @ptrCast(vtab_cursor);
|
|
const nullable_cursor_state: ?*CursorState = @fieldParentPtr("vtab_cursor", vtab_cursor_ptr);
|
|
const cursor_state = nullable_cursor_state orelse unreachable;
|
|
|
|
cursor_state.deinit();
|
|
|
|
return c.SQLITE_OK;
|
|
}
|
|
|
|
fn xEof(vtab_cursor: [*c]c.sqlite3_vtab_cursor) callconv(.c) c_int {
|
|
const vtab_cursor_ptr: *c.sqlite3_vtab_cursor = @ptrCast(vtab_cursor);
|
|
const nullable_cursor_state: ?*CursorState = @fieldParentPtr("vtab_cursor", vtab_cursor_ptr);
|
|
const cursor_state = nullable_cursor_state orelse unreachable;
|
|
const cursor = cursor_state.cursor;
|
|
|
|
var arena = heap.ArenaAllocator.init(cursor_state.module_context.allocator);
|
|
defer arena.deinit();
|
|
|
|
//
|
|
|
|
var diags = VTabDiagnostics{ .allocator = arena.allocator() };
|
|
const has_next = cursor.hasNext(&diags) catch {
|
|
logger.err("unable to call Table.Cursor.hasNext: {s}", .{diags.error_message});
|
|
return 1;
|
|
};
|
|
|
|
if (has_next) {
|
|
return 0;
|
|
} else {
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
const FilterArgsFromCPointerError = error{} || mem.Allocator.Error;
|
|
|
|
fn filterArgsFromCPointer(allocator: mem.Allocator, argc: c_int, argv: [*c]?*c.sqlite3_value) FilterArgsFromCPointerError![]FilterArg {
|
|
const size: usize = @intCast(argc);
|
|
|
|
const res = try allocator.alloc(FilterArg, size);
|
|
for (res, 0..) |*item, i| {
|
|
item.* = .{
|
|
.value = argv[i],
|
|
};
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
fn xFilter(vtab_cursor: [*c]c.sqlite3_vtab_cursor, idx_num: c_int, idx_str: [*c]const u8, argc: c_int, argv: [*c]?*c.sqlite3_value) callconv(.c) c_int {
|
|
const vtab_cursor_ptr: *c.sqlite3_vtab_cursor = @ptrCast(vtab_cursor);
|
|
const nullable_cursor_state: ?*CursorState = @fieldParentPtr("vtab_cursor", vtab_cursor_ptr);
|
|
const cursor_state = nullable_cursor_state orelse unreachable;
|
|
const cursor = cursor_state.cursor;
|
|
|
|
var arena = heap.ArenaAllocator.init(cursor_state.module_context.allocator);
|
|
defer arena.deinit();
|
|
|
|
//
|
|
|
|
const id = IndexIdentifier.fromC(idx_num, idx_str);
|
|
|
|
const args = filterArgsFromCPointer(arena.allocator(), argc, argv) catch |err| {
|
|
logger.err("unable to create filter args, err: {}", .{err});
|
|
return c.SQLITE_ERROR;
|
|
};
|
|
|
|
var diags = VTabDiagnostics{ .allocator = arena.allocator() };
|
|
cursor.filter(&diags, id, args) catch {
|
|
logger.err("unable to call Table.Cursor.filter: {s}", .{diags.error_message});
|
|
return c.SQLITE_ERROR;
|
|
};
|
|
|
|
return c.SQLITE_OK;
|
|
}
|
|
|
|
fn xNext(vtab_cursor: [*c]c.sqlite3_vtab_cursor) callconv(.c) c_int {
|
|
const vtab_cursor_ptr: *c.sqlite3_vtab_cursor = @ptrCast(vtab_cursor);
|
|
const nullable_cursor_state: ?*CursorState = @fieldParentPtr("vtab_cursor", vtab_cursor_ptr);
|
|
const cursor_state = nullable_cursor_state orelse unreachable;
|
|
const cursor = cursor_state.cursor;
|
|
|
|
var arena = heap.ArenaAllocator.init(cursor_state.module_context.allocator);
|
|
defer arena.deinit();
|
|
|
|
//
|
|
|
|
var diags = VTabDiagnostics{ .allocator = arena.allocator() };
|
|
cursor.next(&diags) catch {
|
|
logger.err("unable to call Table.Cursor.next: {s}", .{diags.error_message});
|
|
return c.SQLITE_ERROR;
|
|
};
|
|
|
|
return c.SQLITE_OK;
|
|
}
|
|
|
|
fn xColumn(vtab_cursor: [*c]c.sqlite3_vtab_cursor, ctx: ?*c.sqlite3_context, n: c_int) callconv(.c) c_int {
|
|
const vtab_cursor_ptr: *c.sqlite3_vtab_cursor = @ptrCast(vtab_cursor);
|
|
const nullable_cursor_state: ?*CursorState = @fieldParentPtr("vtab_cursor", vtab_cursor_ptr);
|
|
const cursor_state = nullable_cursor_state orelse unreachable;
|
|
const cursor = cursor_state.cursor;
|
|
|
|
var arena = heap.ArenaAllocator.init(cursor_state.module_context.allocator);
|
|
defer arena.deinit();
|
|
|
|
//
|
|
|
|
var diags = VTabDiagnostics{ .allocator = arena.allocator() };
|
|
const column = cursor.column(&diags, @intCast(n)) catch {
|
|
logger.err("unable to call Table.Cursor.column: {s}", .{diags.error_message});
|
|
return c.SQLITE_ERROR;
|
|
};
|
|
|
|
// TODO(vincent): does it make sense to put this in setResult ? Functions could also return a union.
|
|
const ColumnType = @TypeOf(column);
|
|
switch (@typeInfo(ColumnType)) {
|
|
.@"union" => |info| {
|
|
if (info.tag_type) |UnionTagType| {
|
|
inline for (info.fields) |u_field| {
|
|
|
|
// This wasn't entirely obvious when I saw code like this elsewhere, it works because of type coercion.
|
|
// See https://ziglang.org/documentation/master/#Type-Coercion-unions-and-enums
|
|
const column_tag: std.meta.Tag(ColumnType) = column;
|
|
const this_tag: std.meta.Tag(ColumnType) = @field(UnionTagType, u_field.name);
|
|
|
|
if (column_tag == this_tag) {
|
|
const column_value = @field(column, u_field.name);
|
|
|
|
helpers.setResult(ctx, column_value);
|
|
}
|
|
}
|
|
} else {
|
|
@compileError("cannot use bare unions as a column");
|
|
}
|
|
},
|
|
else => helpers.setResult(ctx, column),
|
|
}
|
|
|
|
return c.SQLITE_OK;
|
|
}
|
|
|
|
fn xRowid(vtab_cursor: [*c]c.sqlite3_vtab_cursor, row_id_ptr: [*c]c.sqlite3_int64) callconv(.c) c_int {
|
|
const vtab_cursor_ptr: *c.sqlite3_vtab_cursor = @ptrCast(vtab_cursor);
|
|
const nullable_cursor_state: ?*CursorState = @fieldParentPtr("vtab_cursor", vtab_cursor_ptr);
|
|
const cursor_state = nullable_cursor_state orelse unreachable;
|
|
const cursor = cursor_state.cursor;
|
|
|
|
var arena = heap.ArenaAllocator.init(cursor_state.module_context.allocator);
|
|
defer arena.deinit();
|
|
|
|
//
|
|
|
|
var diags = VTabDiagnostics{ .allocator = arena.allocator() };
|
|
const row_id = cursor.rowId(&diags) catch {
|
|
logger.err("unable to call Table.Cursor.rowId: {s}", .{diags.error_message});
|
|
return c.SQLITE_ERROR;
|
|
};
|
|
|
|
row_id_ptr.* = row_id;
|
|
|
|
return c.SQLITE_OK;
|
|
}
|
|
};
|
|
}
|
|
|
|
const TestVirtualTable = struct {
|
|
pub const Cursor = TestVirtualTableCursor;
|
|
|
|
const Row = struct {
|
|
foo: []const u8,
|
|
bar: []const u8,
|
|
baz: isize,
|
|
};
|
|
|
|
arena_state: heap.ArenaAllocator.State,
|
|
|
|
rows: []Row,
|
|
schema: [:0]const u8,
|
|
|
|
pub const InitError = error{} || mem.Allocator.Error || fmt.ParseIntError;
|
|
|
|
pub fn init(gpa: mem.Allocator, diags: *VTabDiagnostics, args: []const ModuleArgument) InitError!*TestVirtualTable {
|
|
var arena = heap.ArenaAllocator.init(gpa);
|
|
const allocator = arena.allocator();
|
|
|
|
var res = try allocator.create(TestVirtualTable);
|
|
errdefer res.deinit(gpa);
|
|
|
|
// Generate test data
|
|
const rows = blk: {
|
|
var n: usize = 0;
|
|
for (args) |arg| {
|
|
switch (arg) {
|
|
.plain => {},
|
|
.kv => |kv| {
|
|
if (mem.eql(u8, kv.key, "n")) {
|
|
n = fmt.parseInt(usize, kv.value, 10) catch |err| {
|
|
switch (err) {
|
|
error.InvalidCharacter => diags.setErrorMessage("not a number: {s}", .{kv.value}),
|
|
else => diags.setErrorMessage("got error while parsing value {s}: {}", .{ kv.value, err }),
|
|
}
|
|
return err;
|
|
};
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
//
|
|
|
|
const data = &[_][]const u8{
|
|
"Vincent",
|
|
"José",
|
|
"Michel",
|
|
};
|
|
|
|
var rand = std.Random.DefaultPrng.init(204882485);
|
|
|
|
const tmp = try allocator.alloc(Row, n);
|
|
for (tmp) |*s| {
|
|
const foo_value = data[rand.random().intRangeLessThan(usize, 0, data.len)];
|
|
const bar_value = data[rand.random().intRangeLessThan(usize, 0, data.len)];
|
|
const baz_value = rand.random().intRangeAtMost(isize, 0, 200);
|
|
|
|
s.* = .{
|
|
.foo = foo_value,
|
|
.bar = bar_value,
|
|
.baz = baz_value,
|
|
};
|
|
}
|
|
|
|
break :blk tmp;
|
|
};
|
|
res.rows = rows;
|
|
|
|
// Build the schema
|
|
res.schema = try allocator.dupeZ(u8,
|
|
\\CREATE TABLE foobar(foo TEXT, bar TEXT, baz INTEGER)
|
|
);
|
|
|
|
res.arena_state = arena.state;
|
|
|
|
return res;
|
|
}
|
|
|
|
pub fn deinit(self: *TestVirtualTable, gpa: mem.Allocator) void {
|
|
self.arena_state.promote(gpa).deinit();
|
|
}
|
|
|
|
fn connect(self: *TestVirtualTable) anyerror!void {
|
|
_ = self;
|
|
debug.print("connect\n", .{});
|
|
}
|
|
|
|
pub const BuildBestIndexError = error{} || mem.Allocator.Error || error{WriteFailed};
|
|
|
|
pub fn buildBestIndex(
|
|
self: *TestVirtualTable,
|
|
diags: *VTabDiagnostics,
|
|
builder: *BestIndexBuilder,
|
|
) BuildBestIndexError!void {
|
|
_ = self;
|
|
_ = diags;
|
|
|
|
// var id_str_writer = builder.id_str_buffer.writer(builder.allocator);
|
|
var id_str_writer = std.Io.Writer.fromArrayList(&builder.id_str_buffer);
|
|
|
|
var argv_index: i32 = 0;
|
|
for (builder.constraints) |*constraint| {
|
|
if (constraint.op == .eq) {
|
|
argv_index += 1;
|
|
constraint.usage.argv_index = argv_index;
|
|
|
|
try id_str_writer.print("={d:<6}", .{constraint.column});
|
|
}
|
|
}
|
|
|
|
//
|
|
|
|
builder.id.str = try builder.id_str_buffer.toOwnedSlice(builder.allocator);
|
|
builder.estimated_cost = 200;
|
|
builder.estimated_rows = 200;
|
|
|
|
builder.build();
|
|
}
|
|
|
|
/// An iterator over the rows of this table capable of applying filters.
|
|
/// The filters are used when the index asks for it.
|
|
const Iterator = struct {
|
|
rows: []Row,
|
|
pos: usize,
|
|
|
|
filters: struct {
|
|
foo: ?[]const u8 = null,
|
|
bar: ?[]const u8 = null,
|
|
} = .{},
|
|
|
|
fn init(rows: []Row) Iterator {
|
|
return Iterator{
|
|
.rows = rows,
|
|
.pos = 0,
|
|
};
|
|
}
|
|
|
|
fn currentRow(it: *Iterator) Row {
|
|
return it.rows[it.pos];
|
|
}
|
|
|
|
fn hasNext(it: *Iterator) bool {
|
|
return it.pos < it.rows.len;
|
|
}
|
|
|
|
fn next(it: *Iterator) void {
|
|
const foo = it.filters.foo orelse "";
|
|
const bar = it.filters.bar orelse "";
|
|
|
|
it.pos += 1;
|
|
|
|
while (it.pos < it.rows.len) : (it.pos += 1) {
|
|
const row = it.rows[it.pos];
|
|
|
|
if (foo.len > 0 and bar.len > 0 and mem.eql(u8, foo, row.foo) and mem.eql(u8, bar, row.bar)) break;
|
|
if (foo.len > 0 and mem.eql(u8, foo, row.foo)) break;
|
|
if (bar.len > 0 and mem.eql(u8, bar, row.bar)) break;
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
const TestVirtualTableCursor = struct {
|
|
allocator: mem.Allocator,
|
|
parent: *TestVirtualTable,
|
|
iterator: TestVirtualTable.Iterator,
|
|
|
|
pub const InitError = error{} || mem.Allocator.Error;
|
|
|
|
pub fn init(allocator: mem.Allocator, parent: *TestVirtualTable) InitError!*TestVirtualTableCursor {
|
|
const res = try allocator.create(TestVirtualTableCursor);
|
|
res.* = .{
|
|
.allocator = allocator,
|
|
.parent = parent,
|
|
.iterator = TestVirtualTable.Iterator.init(parent.rows),
|
|
};
|
|
return res;
|
|
}
|
|
|
|
pub fn deinit(cursor: *TestVirtualTableCursor) void {
|
|
cursor.allocator.destroy(cursor);
|
|
}
|
|
|
|
pub const FilterError = error{InvalidColumn} || fmt.ParseIntError;
|
|
|
|
pub fn filter(cursor: *TestVirtualTableCursor, diags: *VTabDiagnostics, index: IndexIdentifier, args: []FilterArg) FilterError!void {
|
|
_ = diags;
|
|
|
|
var id = index.str;
|
|
|
|
// NOTE(vincent): this is an ugly ass parser for the index string, don't judge me.
|
|
|
|
var i: usize = 0;
|
|
while (true) {
|
|
const pos = mem.indexOfScalar(u8, id, '=') orelse break;
|
|
|
|
const arg = args[i];
|
|
i += 1;
|
|
|
|
// 3 chars for the '=' marker
|
|
// 6 chars because we format all columns in a 6 char wide string
|
|
const col_str = id[pos + 1 .. pos + 1 + 6];
|
|
const col = try fmt.parseInt(i32, mem.trimEnd(u8, col_str, " "), 10);
|
|
|
|
id = id[pos + 1 + 6 ..];
|
|
|
|
//
|
|
|
|
if (col == 0) {
|
|
cursor.iterator.filters.foo = arg.as([]const u8);
|
|
} else if (col == 1) {
|
|
cursor.iterator.filters.bar = arg.as([]const u8);
|
|
} else if (col == 2) {
|
|
_ = arg.as(isize);
|
|
} else {
|
|
return error.InvalidColumn;
|
|
}
|
|
}
|
|
}
|
|
|
|
pub const NextError = error{};
|
|
|
|
pub fn next(cursor: *TestVirtualTableCursor, diags: *VTabDiagnostics) NextError!void {
|
|
_ = diags;
|
|
|
|
cursor.iterator.next();
|
|
}
|
|
|
|
pub const HasNextError = error{};
|
|
|
|
pub fn hasNext(cursor: *TestVirtualTableCursor, diags: *VTabDiagnostics) HasNextError!bool {
|
|
_ = diags;
|
|
|
|
return cursor.iterator.hasNext();
|
|
}
|
|
|
|
pub const Column = union(enum) {
|
|
foo: []const u8,
|
|
bar: []const u8,
|
|
baz: isize,
|
|
};
|
|
|
|
pub const ColumnError = error{InvalidColumn};
|
|
|
|
pub fn column(cursor: *TestVirtualTableCursor, diags: *VTabDiagnostics, column_number: i32) ColumnError!Column {
|
|
_ = diags;
|
|
|
|
const row = cursor.iterator.currentRow();
|
|
|
|
switch (column_number) {
|
|
0 => return Column{ .foo = row.foo },
|
|
1 => return Column{ .bar = row.bar },
|
|
2 => return Column{ .baz = row.baz },
|
|
else => return error.InvalidColumn,
|
|
}
|
|
}
|
|
|
|
pub const RowIDError = error{};
|
|
|
|
pub fn rowId(cursor: *TestVirtualTableCursor, diags: *VTabDiagnostics) RowIDError!i64 {
|
|
_ = diags;
|
|
|
|
return @intCast(cursor.iterator.pos);
|
|
}
|
|
};
|
|
|
|
test "virtual table" {
|
|
var db = try getTestDb();
|
|
defer db.deinit();
|
|
|
|
var myvtab_module_context = ModuleContext{
|
|
.allocator = testing.allocator,
|
|
};
|
|
|
|
try db.createVirtualTable(
|
|
"myvtab",
|
|
&myvtab_module_context,
|
|
TestVirtualTable,
|
|
);
|
|
|
|
var diags = Diagnostics{};
|
|
try db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS vtab_foobar USING myvtab(n=200)", .{ .diags = &diags }, .{});
|
|
|
|
// Filter with both `foo` and `bar`
|
|
|
|
var stmt = try db.prepareWithDiags(
|
|
"SELECT rowid, foo, bar, baz FROM vtab_foobar WHERE foo = ?{[]const u8} AND bar = ?{[]const u8} AND baz > ?{usize}",
|
|
.{ .diags = &diags },
|
|
);
|
|
defer stmt.deinit();
|
|
|
|
var rows_arena = heap.ArenaAllocator.init(testing.allocator);
|
|
defer rows_arena.deinit();
|
|
|
|
const rows = try stmt.all(
|
|
struct {
|
|
id: i64,
|
|
foo: []const u8,
|
|
bar: []const u8,
|
|
baz: usize,
|
|
},
|
|
rows_arena.allocator(),
|
|
.{ .diags = &diags },
|
|
.{
|
|
.foo = @as([]const u8, "Vincent"),
|
|
.bar = @as([]const u8, "Michel"),
|
|
.baz = @as(usize, 2),
|
|
},
|
|
);
|
|
try testing.expect(rows.len > 0);
|
|
|
|
for (rows) |row| {
|
|
try testing.expectEqualStrings("Vincent", row.foo);
|
|
try testing.expectEqualStrings("Michel", row.bar);
|
|
try testing.expect(row.baz > 2);
|
|
}
|
|
}
|
|
|
|
test "parse module arguments" {
|
|
var arena = heap.ArenaAllocator.init(testing.allocator);
|
|
defer arena.deinit();
|
|
const allocator = arena.allocator();
|
|
|
|
const args = try allocator.alloc([*c]const u8, 20);
|
|
for (args, 0..) |*arg, i| {
|
|
const tmp = try fmt.allocPrintSentinel(allocator, "arg={d}", .{i}, 0);
|
|
arg.* = @ptrCast(tmp);
|
|
}
|
|
|
|
const res = try parseModuleArguments(
|
|
allocator,
|
|
@intCast(args.len),
|
|
@ptrCast(args),
|
|
);
|
|
try testing.expectEqual(@as(usize, 20), res.len);
|
|
|
|
for (res, 0..) |arg, i| {
|
|
try testing.expectEqualStrings("arg", arg.kv.key);
|
|
try testing.expectEqual(i, try fmt.parseInt(usize, arg.kv.value, 10));
|
|
}
|
|
}
|