From 6ca5a280f4dc6d248f829cc76abe3149b646cea5 Mon Sep 17 00:00:00 2001 From: Vincent Rischmann Date: Mon, 15 Aug 2022 21:53:24 +0200 Subject: add the virtual table implementation --- sqlite.zig | 25 ++ vtab.zig | 1275 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1300 insertions(+) create mode 100644 vtab.zig diff --git a/sqlite.zig b/sqlite.zig index 6af1d04..1b75bc5 100644 --- a/sqlite.zig +++ b/sqlite.zig @@ -20,8 +20,13 @@ const getLastDetailedErrorFromDb = errors.getLastDetailedErrorFromDb; const getDetailedErrorFromResultCode = errors.getDetailedErrorFromResultCode; const getTestDb = @import("test.zig").getTestDb; +pub const vtab = @import("vtab.zig"); const helpers = @import("helpers.zig"); +test { + _ = @import("vtab.zig"); +} + const logger = std.log.scoped(.sqlite); /// Text is used to represent a SQLite TEXT value when binding a parameter or reading a column. @@ -787,6 +792,26 @@ pub const Db = struct { try stmt.exec(new_options, .{}); } } + + pub fn createVirtualTable( + self: *Self, + comptime name: [:0]const u8, + module_context: *vtab.ModuleContext, + comptime Table: type, + ) !void { + const VirtualTableType = vtab.VirtualTable(name, Table); + + const result = c.sqlite3_create_module_v2( + self.db, + name, + &VirtualTableType.module, + module_context, + null, + ); + if (result != c.SQLITE_OK) { + return errors.errorFromResultCode(result); + } + } }; /// FunctionContext is the context passed as first parameter in the `step` and `finalize` functions used with `createAggregateFunction`. diff --git a/vtab.zig b/vtab.zig new file mode 100644 index 0000000..ba71d46 --- /dev/null +++ b/vtab.zig @@ -0,0 +1,1275 @@ +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); + +/// 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, +}; + +const StringSliceFromCPointerError = error{} || mem.Allocator.Error; + +/// Converts an array of arguments coming from sqlite to a slice of slice which makes it easier to use. +fn stringSliceFromCPointer(allocator: mem.Allocator, argc: c_int, argv: [*c]const [*c]const u8) StringSliceFromCPointerError![]const []const u8 { + const size = @intCast(usize, argc); + + var res = try allocator.alloc([]const u8, size); + for (res) |*slice, i| { + // The documentation of sqlite says each string in argv is null-terminated + slice.* = mem.sliceTo(argv[i], 0); + } + + return res; +} + +fn dupeToSQLiteString(s: []const u8) [*c]const u8 { + var buffer = @ptrCast([*c]u8, c.sqlite3_malloc(@intCast(c_int, s.len) + 1)); + + mem.copy(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.allocPrint(self.allocator, format_string, values) 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 { + var res = Self{ + .allocator = allocator, + .index_info = index_info, + .id_str_buffer = std.ArrayList(u8).init(allocator), + .constraints = try allocator.alloc(Constraint, @intCast(usize, index_info.nConstraint)), + .columns_used = @intCast(u64, index_info.colUsed), + .id = .{}, + }; + + for (res.constraints) |*constraint, i| { + const raw_constraint = index_info.aConstraint[i]; + + constraint.column = @intCast(isize, 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: []c.sqlite3_index_constraint_usage = index_info.aConstraintUsage[0..self.constraints.len]; + for (self.constraints) |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(c_int, self.id.num); + if (self.id.str.len > 0) { + // Must always be NULL-terminated so add 1 + const tmp = @ptrCast([*c]u8, c.sqlite3_malloc(@intCast(c_int, self.id.str.len + 1))); + + mem.copy(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(i32, 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 (!meta.trait.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 (!meta.trait.hasFn("init")(Cursor)) { + @compileError("the Cursor type must have an init function, " ++ error_message); + } + + const info = @typeInfo(@TypeOf(Cursor.init)).Fn; + + if (info.args.len != 2) @compileError(error_message); + if (info.args[0].arg_type.? != mem.Allocator) @compileError(error_message); + if (info.args[1].arg_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 (!meta.trait.hasFn("deinit")(Cursor)) { + @compileError("the Cursor type must have a deinit function, " ++ error_message); + } + + const info = @typeInfo(@TypeOf(Cursor.deinit)).Fn; + + if (info.args.len != 1) @compileError(error_message); + if (info.args[0].arg_type.? != *Cursor) @compileError(error_message); + if (info.return_type.? != void) @compileError(error_message); + } + + // Validate the `next` function + { + if (!meta.trait.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 (!meta.trait.hasFn("next")(Cursor)) { + @compileError("the Cursor type must have a next function, " ++ error_message); + } + + const info = @typeInfo(@TypeOf(Cursor.next)).Fn; + + if (info.args.len != 2) @compileError(error_message); + if (info.args[0].arg_type.? != *Cursor) @compileError(error_message); + if (info.args[1].arg_type.? != *VTabDiagnostics) @compileError(error_message); + if (info.return_type.? != Cursor.NextError!void) @compileError(error_message); + } + + // Validate the `hasNext` function + { + if (!meta.trait.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 (!meta.trait.hasFn("hasNext")(Cursor)) { + @compileError("the Cursor type must have a hasNext function, " ++ error_message); + } + + const info = @typeInfo(@TypeOf(Cursor.hasNext)).Fn; + + if (info.args.len != 2) @compileError(error_message); + if (info.args[0].arg_type.? != *Cursor) @compileError(error_message); + if (info.args[1].arg_type.? != *VTabDiagnostics) @compileError(error_message); + if (info.return_type.? != Cursor.HasNextError!bool) @compileError(error_message); + } + + // Validate the `filter` function + { + if (!meta.trait.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 (!meta.trait.hasFn("filter")(Cursor)) { + @compileError("the Cursor type must have a filter function, " ++ error_message); + } + + const info = @typeInfo(@TypeOf(Cursor.filter)).Fn; + + if (info.args.len != 4) @compileError(error_message); + if (info.args[0].arg_type.? != *Cursor) @compileError(error_message); + if (info.args[1].arg_type.? != *VTabDiagnostics) @compileError(error_message); + if (info.args[2].arg_type.? != IndexIdentifier) @compileError(error_message); + if (info.args[3].arg_type.? != []FilterArg) @compileError(error_message); + if (info.return_type.? != Cursor.FilterError!void) @compileError(error_message); + } + + // Validate the `column` function + { + if (!meta.trait.hasDecls(Cursor, .{"ColumnError"})) { + @compileError("the Cursor type must declare a ColumnError error set for the column function"); + } + if (!meta.trait.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 (!meta.trait.hasFn("column")(Cursor)) { + @compileError("the Cursor type must have a column function, " ++ error_message); + } + + const info = @typeInfo(@TypeOf(Cursor.column)).Fn; + + if (info.args.len != 3) @compileError(error_message); + if (info.args[0].arg_type.? != *Cursor) @compileError(error_message); + if (info.args[1].arg_type.? != *VTabDiagnostics) @compileError(error_message); + if (info.args[2].arg_type.? != i32) @compileError(error_message); + if (info.return_type.? != Cursor.ColumnError!Cursor.Column) @compileError(error_message); + } + + // Validate the `rowId` function + { + if (!meta.trait.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 (!meta.trait.hasFn("rowId")(Cursor)) { + @compileError("the Cursor type must have a rowId function, " ++ error_message); + } + + const info = @typeInfo(@TypeOf(Cursor.rowId)).Fn; + + if (info.args.len != 2) @compileError(error_message); + if (info.args[0].arg_type.? != *Cursor) @compileError(error_message); + if (info.args[1].arg_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 (!meta.trait.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) InitError!*Table` + ; + + if (!meta.trait.hasFn("init")(Table)) { + @compileError("the Table type must have a init function, " ++ error_message); + } + + const info = @typeInfo(@TypeOf(Table.init)).Fn; + + if (info.args.len != 3) @compileError(error_message); + if (info.args[0].arg_type.? != mem.Allocator) @compileError(error_message); + if (info.args[1].arg_type.? != *VTabDiagnostics) @compileError(error_message); + // TODO(vincent): maybe allow a signature without the args since a table can do withoout them + if (info.args[2].arg_type.? != []const []const u8) @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 (!meta.trait.hasFn("deinit")(Table)) { + @compileError("the Table type must have a deinit function, " ++ error_message); + } + + const info = @typeInfo(@TypeOf(Table.deinit)).Fn; + + if (info.args.len != 2) @compileError(error_message); + if (info.args[0].arg_type.? != *Table) @compileError(error_message); + if (info.args[1].arg_type.? != mem.Allocator) @compileError(error_message); + if (info.return_type.? != void) @compileError(error_message); + } + + // Validate the `buildBestIndex` function + { + if (!meta.trait.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 (!meta.trait.hasFn("buildBestIndex")(Table)) { + @compileError("the Table type must have a buildBestIndex function, " ++ error_message); + } + + const info = @typeInfo(@TypeOf(Table.buildBestIndex)).Fn; + + if (info.args.len != 3) @compileError(error_message); + if (info.args[0].arg_type.? != *Table) @compileError(error_message); + if (info.args[1].arg_type.? != *VTabDiagnostics) @compileError(error_message); + if (info.args[2].arg_type.? != *BestIndexBuilder) @compileError(error_message); + if (info.return_type.? != Table.BuildBestIndexError!void) @compileError(error_message); + } + + if (!meta.trait.hasDecls(Table, .{"Cursor"})) { + @compileError("the Table type must declare a Cursor type"); + } +} + +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 { + var 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 { + var 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(*ModuleContext, @alignCast(@alignOf(ModuleContext), ptr.?)); + } + + fn createState(allocator: mem.Allocator, diags: *VTabDiagnostics, module_context: *ModuleContext, args: []const []const u8) !*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]const 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. + // TODO(vincent): maybe we should provide a way to automatically parse arguments + const args = stringSliceFromCPointer(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(*c.sqlite3_vtab, state); + + const res = c.sqlite3_declare_vtab(db, @ptrCast([*c]const u8, 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 state = @fieldParentPtr(State, "vtab", vtab); + + 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 state = @fieldParentPtr(State, "vtab", vtab); + 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 state = @fieldParentPtr(State, "vtab", vtab); + + 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(*c.sqlite3_vtab_cursor, cursor_state); + + return c.SQLITE_OK; + } + + fn xClose(vtab_cursor: [*c]c.sqlite3_vtab_cursor) callconv(.C) c_int { + const cursor_state = @fieldParentPtr(CursorState, "vtab_cursor", vtab_cursor); + cursor_state.deinit(); + + return c.SQLITE_OK; + } + + fn xEof(vtab_cursor: [*c]c.sqlite3_vtab_cursor) callconv(.C) c_int { + const cursor_state = @fieldParentPtr(CursorState, "vtab_cursor", vtab_cursor); + 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 = @intCast(usize, argc); + + var res = try allocator.alloc(FilterArg, size); + for (res) |*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 cursor_state = @fieldParentPtr(CursorState, "vtab_cursor", vtab_cursor); + 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); + + var 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 cursor_state = @fieldParentPtr(CursorState, "vtab_cursor", vtab_cursor); + 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 cursor_state = @fieldParentPtr(CursorState, "vtab_cursor", vtab_cursor); + 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(i32, 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 cursor_state = @fieldParentPtr(CursorState, "vtab_cursor", vtab_cursor); + 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 []const u8) 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 + // TODO(vincent): maybe we should provide a way to automatically parse arguments + const rows = blk: { + var content: []const u8 = ""; + var n: usize = 0; + + for (args) |arg| { + if (mem.startsWith(u8, arg, "content=")) { + const pos = mem.indexOfScalar(u8, arg, '=') orelse continue; + content = arg[pos + 1 ..]; + } else if (mem.startsWith(u8, arg, "n=")) { + const pos = mem.indexOfScalar(u8, arg, '=') orelse continue; + const value = arg[pos + 1 ..]; + n = fmt.parseInt(usize, value, 10) catch |err| { + switch (err) { + error.InvalidCharacter => diags.setErrorMessage("not a number: {s}", .{value}), + else => diags.setErrorMessage("got error while parsing value {s}: {!}", .{ value, err }), + } + return err; + }; + } + } + + // TODO-vincent): stop using random data so we can actually test + const data = &[_][]const u8{ + "Vincent", "José", "Michel", + }; + + var rand = std.rand.DefaultPrng.init(204882485); + + var 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 = 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 { + var 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 { + _ = cursor; + _ = diags; + _ = index; + _ = args; + + debug.print("idx num: {d}\n", .{index.num}); + debug.print("idx str: {s}\n", .{index.str}); + + 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; + } + } + + debug.print("expected args: {d}\n", .{i}); + } + + 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 { + _ = cursor; + _ = 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(i64, 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 foobar USING myvtab(content=hello, n=200)", .{ .diags = &diags }, .{}); + + // Filter with both `foo` and `bar` + + var stmt = try db.prepareWithDiags( + "SELECT rowid, foo, bar, baz FROM 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| { + debug.print("result row: id={d} foo={s} bar={s} baz={d}\n", .{ row.id, row.foo, row.bar, row.baz }); + } + + // const row1 = rows[0]; + // try testing.expectEqual(@as(i64, 0), row1.id); + // try testing.expectEqualStrings("foo_hello", row1.foo); + // try testing.expectEqualStrings("bar_hello", row1.bar); + // try testing.expectEqual(@as(usize, 0), row1.baz); + + // const row2 = rows[1]; + // try testing.expectEqual(@as(i64, 1), row2.id); + // try testing.expectEqualStrings("foo_hello", row2.foo); + // try testing.expectEqualStrings("bar_hello", row2.bar); + // try testing.expectEqual(@as(usize, 1), row2.baz); +} -- cgit v1.2.3