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 = std.ArrayList(u8).init(allocator), .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 nullable_state: ?*State = @fieldParentPtr("vtab", vtab); 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 nullable_state: ?*State = @fieldParentPtr("vtab", vtab); 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 nullable_state: ?*State = @fieldParentPtr("vtab", vtab); 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 nullable_cursor_state: ?*CursorState = @fieldParentPtr("vtab_cursor", vtab_cursor); 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 nullable_cursor_state: ?*CursorState = @fieldParentPtr("vtab_cursor", vtab_cursor); 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 nullable_cursor_state: ?*CursorState = @fieldParentPtr("vtab_cursor", vtab_cursor); 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 nullable_cursor_state: ?*CursorState = @fieldParentPtr("vtab_cursor", vtab_cursor); 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 nullable_cursor_state: ?*CursorState = @fieldParentPtr("vtab_cursor", vtab_cursor); 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 nullable_cursor_state: ?*CursorState = @fieldParentPtr("vtab_cursor", vtab_cursor); 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; pub fn buildBestIndex(self: *TestVirtualTable, diags: *VTabDiagnostics, builder: *BestIndexBuilder) BuildBestIndexError!void { _ = self; _ = diags; var id_str_writer = builder.id_str_buffer.writer(); 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.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.trimRight(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)); } }