diff options
Diffstat (limited to 'vtab.zig')
| -rw-r--r-- | vtab.zig | 1275 |
1 files changed, 1275 insertions, 0 deletions
diff --git a/vtab.zig b/vtab.zig new file mode 100644 index 0000000..ba71d46 --- /dev/null +++ b/vtab.zig | |||
| @@ -0,0 +1,1275 @@ | |||
| 1 | const std = @import("std"); | ||
| 2 | const debug = std.debug; | ||
| 3 | const fmt = std.fmt; | ||
| 4 | const heap = std.heap; | ||
| 5 | const mem = std.mem; | ||
| 6 | const meta = std.meta; | ||
| 7 | const testing = std.testing; | ||
| 8 | |||
| 9 | const c = @import("c.zig").c; | ||
| 10 | const versionGreaterThanOrEqualTo = @import("c.zig").versionGreaterThanOrEqualTo; | ||
| 11 | const getTestDb = @import("test.zig").getTestDb; | ||
| 12 | const Diagnostics = @import("sqlite.zig").Diagnostics; | ||
| 13 | const Blob = @import("sqlite.zig").Blob; | ||
| 14 | const Text = @import("sqlite.zig").Text; | ||
| 15 | const helpers = @import("helpers.zig"); | ||
| 16 | |||
| 17 | const logger = std.log.scoped(.vtab); | ||
| 18 | |||
| 19 | /// ModuleContext contains state that is needed by all implementations of virtual tables. | ||
| 20 | /// | ||
| 21 | /// Currently there's only an allocator. | ||
| 22 | pub const ModuleContext = struct { | ||
| 23 | allocator: mem.Allocator, | ||
| 24 | }; | ||
| 25 | |||
| 26 | const StringSliceFromCPointerError = error{} || mem.Allocator.Error; | ||
| 27 | |||
| 28 | /// Converts an array of arguments coming from sqlite to a slice of slice which makes it easier to use. | ||
| 29 | fn stringSliceFromCPointer(allocator: mem.Allocator, argc: c_int, argv: [*c]const [*c]const u8) StringSliceFromCPointerError![]const []const u8 { | ||
| 30 | const size = @intCast(usize, argc); | ||
| 31 | |||
| 32 | var res = try allocator.alloc([]const u8, size); | ||
| 33 | for (res) |*slice, i| { | ||
| 34 | // The documentation of sqlite says each string in argv is null-terminated | ||
| 35 | slice.* = mem.sliceTo(argv[i], 0); | ||
| 36 | } | ||
| 37 | |||
| 38 | return res; | ||
| 39 | } | ||
| 40 | |||
| 41 | fn dupeToSQLiteString(s: []const u8) [*c]const u8 { | ||
| 42 | var buffer = @ptrCast([*c]u8, c.sqlite3_malloc(@intCast(c_int, s.len) + 1)); | ||
| 43 | |||
| 44 | mem.copy(u8, buffer[0..s.len], s); | ||
| 45 | buffer[s.len] = 0; | ||
| 46 | |||
| 47 | return buffer; | ||
| 48 | } | ||
| 49 | |||
| 50 | /// VTabDiagnostics is used by the user to report error diagnostics to the virtual table. | ||
| 51 | pub const VTabDiagnostics = struct { | ||
| 52 | const Self = @This(); | ||
| 53 | |||
| 54 | allocator: mem.Allocator, | ||
| 55 | |||
| 56 | error_message: []const u8 = "unknown error", | ||
| 57 | |||
| 58 | pub fn setErrorMessage(self: *Self, comptime format_string: []const u8, values: anytype) void { | ||
| 59 | self.error_message = fmt.allocPrint(self.allocator, format_string, values) catch |err| switch (err) { | ||
| 60 | error.OutOfMemory => "can't set diagnostic message, out of memory", | ||
| 61 | }; | ||
| 62 | } | ||
| 63 | }; | ||
| 64 | |||
| 65 | pub const BestIndexBuilder = struct { | ||
| 66 | const Self = @This(); | ||
| 67 | |||
| 68 | /// Constraint operator codes. | ||
| 69 | /// See https://sqlite.org/c3ref/c_index_constraint_eq.html | ||
| 70 | pub const ConstraintOp = if (versionGreaterThanOrEqualTo(3, 38, 0)) | ||
| 71 | enum { | ||
| 72 | eq, | ||
| 73 | gt, | ||
| 74 | le, | ||
| 75 | lt, | ||
| 76 | ge, | ||
| 77 | match, | ||
| 78 | like, | ||
| 79 | glob, | ||
| 80 | regexp, | ||
| 81 | ne, | ||
| 82 | is_not, | ||
| 83 | is_not_null, | ||
| 84 | is_null, | ||
| 85 | is, | ||
| 86 | limit, | ||
| 87 | offset, | ||
| 88 | } | ||
| 89 | else | ||
| 90 | enum { | ||
| 91 | eq, | ||
| 92 | gt, | ||
| 93 | le, | ||
| 94 | lt, | ||
| 95 | ge, | ||
| 96 | match, | ||
| 97 | like, | ||
| 98 | glob, | ||
| 99 | regexp, | ||
| 100 | ne, | ||
| 101 | is_not, | ||
| 102 | is_not_null, | ||
| 103 | is_null, | ||
| 104 | is, | ||
| 105 | }; | ||
| 106 | |||
| 107 | const ConstraintOpFromCodeError = error{ | ||
| 108 | InvalidCode, | ||
| 109 | }; | ||
| 110 | |||
| 111 | fn constraintOpFromCode(code: u8) ConstraintOpFromCodeError!ConstraintOp { | ||
| 112 | if (comptime versionGreaterThanOrEqualTo(3, 38, 0)) { | ||
| 113 | switch (code) { | ||
| 114 | c.SQLITE_INDEX_CONSTRAINT_LIMIT => return .limit, | ||
| 115 | c.SQLITE_INDEX_CONSTRAINT_OFFSET => return .offset, | ||
| 116 | else => {}, | ||
| 117 | } | ||
| 118 | } | ||
| 119 | |||
| 120 | switch (code) { | ||
| 121 | c.SQLITE_INDEX_CONSTRAINT_EQ => return .eq, | ||
| 122 | c.SQLITE_INDEX_CONSTRAINT_GT => return .gt, | ||
| 123 | c.SQLITE_INDEX_CONSTRAINT_LE => return .le, | ||
| 124 | c.SQLITE_INDEX_CONSTRAINT_LT => return .lt, | ||
| 125 | c.SQLITE_INDEX_CONSTRAINT_GE => return .ge, | ||
| 126 | c.SQLITE_INDEX_CONSTRAINT_MATCH => return .match, | ||
| 127 | c.SQLITE_INDEX_CONSTRAINT_LIKE => return .like, | ||
| 128 | c.SQLITE_INDEX_CONSTRAINT_GLOB => return .glob, | ||
| 129 | c.SQLITE_INDEX_CONSTRAINT_REGEXP => return .regexp, | ||
| 130 | c.SQLITE_INDEX_CONSTRAINT_NE => return .ne, | ||
| 131 | c.SQLITE_INDEX_CONSTRAINT_ISNOT => return .is_not, | ||
| 132 | c.SQLITE_INDEX_CONSTRAINT_ISNOTNULL => return .is_not_null, | ||
| 133 | c.SQLITE_INDEX_CONSTRAINT_ISNULL => return .is_null, | ||
| 134 | c.SQLITE_INDEX_CONSTRAINT_IS => return .is, | ||
| 135 | else => return error.InvalidCode, | ||
| 136 | } | ||
| 137 | } | ||
| 138 | |||
| 139 | // WHERE clause constraint | ||
| 140 | pub const Constraint = struct { | ||
| 141 | // Column constrained. -1 for ROWID | ||
| 142 | column: isize, | ||
| 143 | op: ConstraintOp, | ||
| 144 | usable: bool, | ||
| 145 | |||
| 146 | usage: struct { | ||
| 147 | // If >0, constraint is part of argv to xFilter | ||
| 148 | argv_index: i32 = 0, | ||
| 149 | // Id >0, do not code a test for this constraint | ||
| 150 | omit: bool = false, | ||
| 151 | }, | ||
| 152 | }; | ||
| 153 | |||
| 154 | // ORDER BY clause | ||
| 155 | pub const OrderBy = struct { | ||
| 156 | column: usize, | ||
| 157 | order: enum { | ||
| 158 | desc, | ||
| 159 | asc, | ||
| 160 | }, | ||
| 161 | }; | ||
| 162 | |||
| 163 | /// Internal state | ||
| 164 | allocator: mem.Allocator, | ||
| 165 | id_str_buffer: std.ArrayList(u8), | ||
| 166 | index_info: *c.sqlite3_index_info, | ||
| 167 | |||
| 168 | /// List of WHERE clause constraints | ||
| 169 | /// | ||
| 170 | /// Similar to `aConstraint` in the Inputs section of sqlite3_index_info except we embed the constraint usage in there too. | ||
| 171 | /// This makes it nicer to use for the user. | ||
| 172 | constraints: []Constraint, | ||
| 173 | |||
| 174 | /// Indicate which columns of the virtual table are actually used by the statement. | ||
| 175 | /// If the lowest bit of colUsed is set, that means that the first column is used. | ||
| 176 | /// The second lowest bit corresponds to the second column. And so forth. | ||
| 177 | /// | ||
| 178 | /// Maps to the `colUsed` field. | ||
| 179 | columns_used: u64, | ||
| 180 | |||
| 181 | /// Index identifier. | ||
| 182 | /// This is passed to the filtering function to identify which index to use. | ||
| 183 | /// | ||
| 184 | /// Maps to the `idxNum` and `idxStr` field in sqlite3_index_info. | ||
| 185 | /// Id id.id_str is non empty the string will be copied to a SQLite-allocated buffer and `needToFreeIdxStr` will be 1. | ||
| 186 | id: IndexIdentifier, | ||
| 187 | |||
| 188 | /// 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. | ||
| 189 | /// This will indicate to SQLite that it doesn't need to do a sorting pass. | ||
| 190 | /// | ||
| 191 | /// Maps to the `orderByConsumed` field. | ||
| 192 | already_ordered: bool = false, | ||
| 193 | |||
| 194 | /// Estimated number of "disk access operations" required to execute this query. | ||
| 195 | /// | ||
| 196 | /// Maps to the `estimatedCost` field. | ||
| 197 | estimated_cost: ?f64 = null, | ||
| 198 | |||
| 199 | /// Estimated number of rows returned by this query. | ||
| 200 | /// | ||
| 201 | /// Maps to the `estimatedRows` field. | ||
| 202 | /// | ||
| 203 | /// ODO(vincent): implement this | ||
| 204 | estimated_rows: ?i64 = null, | ||
| 205 | |||
| 206 | /// Additiounal flags for this index. | ||
| 207 | /// | ||
| 208 | /// Maps to the `idxFlags` field. | ||
| 209 | flags: struct { | ||
| 210 | unique: bool = false, | ||
| 211 | } = .{}, | ||
| 212 | |||
| 213 | const InitError = error{} || mem.Allocator.Error || ConstraintOpFromCodeError; | ||
| 214 | |||
| 215 | fn init(allocator: mem.Allocator, index_info: *c.sqlite3_index_info) InitError!Self { | ||
| 216 | var res = Self{ | ||
| 217 | .allocator = allocator, | ||
| 218 | .index_info = index_info, | ||
| 219 | .id_str_buffer = std.ArrayList(u8).init(allocator), | ||
| 220 | .constraints = try allocator.alloc(Constraint, @intCast(usize, index_info.nConstraint)), | ||
| 221 | .columns_used = @intCast(u64, index_info.colUsed), | ||
| 222 | .id = .{}, | ||
| 223 | }; | ||
| 224 | |||
| 225 | for (res.constraints) |*constraint, i| { | ||
| 226 | const raw_constraint = index_info.aConstraint[i]; | ||
| 227 | |||
| 228 | constraint.column = @intCast(isize, raw_constraint.iColumn); | ||
| 229 | constraint.op = try constraintOpFromCode(raw_constraint.op); | ||
| 230 | constraint.usable = if (raw_constraint.usable == 1) true else false; | ||
| 231 | constraint.usage = .{}; | ||
| 232 | } | ||
| 233 | |||
| 234 | return res; | ||
| 235 | } | ||
| 236 | |||
| 237 | /// Returns true if the column is used, false otherwise. | ||
| 238 | pub fn isColumnUsed(self: *Self, column: u6) bool { | ||
| 239 | const mask = @as(u64, 1) << column - 1; | ||
| 240 | return self.columns_used & mask == mask; | ||
| 241 | } | ||
| 242 | |||
| 243 | /// Builds the final index data. | ||
| 244 | /// | ||
| 245 | /// Internally it populates the sqlite3_index_info "Outputs" fields using the information set by the user. | ||
| 246 | pub fn build(self: *Self) void { | ||
| 247 | var index_info = self.index_info; | ||
| 248 | |||
| 249 | // Populate the constraint usage | ||
| 250 | var constraint_usage: []c.sqlite3_index_constraint_usage = index_info.aConstraintUsage[0..self.constraints.len]; | ||
| 251 | for (self.constraints) |constraint, i| { | ||
| 252 | constraint_usage[i].argvIndex = constraint.usage.argv_index; | ||
| 253 | constraint_usage[i].omit = if (constraint.usage.omit) 1 else 0; | ||
| 254 | } | ||
| 255 | |||
| 256 | // Identifiers | ||
| 257 | index_info.idxNum = @intCast(c_int, self.id.num); | ||
| 258 | if (self.id.str.len > 0) { | ||
| 259 | // Must always be NULL-terminated so add 1 | ||
| 260 | const tmp = @ptrCast([*c]u8, c.sqlite3_malloc(@intCast(c_int, self.id.str.len + 1))); | ||
| 261 | |||
| 262 | mem.copy(u8, tmp[0..self.id.str.len], self.id.str); | ||
| 263 | tmp[self.id.str.len] = 0; | ||
| 264 | |||
| 265 | index_info.idxStr = tmp; | ||
| 266 | index_info.needToFreeIdxStr = 1; | ||
| 267 | } | ||
| 268 | |||
| 269 | index_info.orderByConsumed = if (self.already_ordered) 1 else 0; | ||
| 270 | if (self.estimated_cost) |estimated_cost| { | ||
| 271 | index_info.estimatedCost = estimated_cost; | ||
| 272 | } | ||
| 273 | if (self.estimated_rows) |estimated_rows| { | ||
| 274 | index_info.estimatedRows = estimated_rows; | ||
| 275 | } | ||
| 276 | |||
| 277 | // Flags | ||
| 278 | index_info.idxFlags = 0; | ||
| 279 | if (self.flags.unique) { | ||
| 280 | index_info.idxFlags |= c.SQLITE_INDEX_SCAN_UNIQUE; | ||
| 281 | } | ||
| 282 | } | ||
| 283 | }; | ||
| 284 | |||
| 285 | /// Identifies an index for a virtual table. | ||
| 286 | /// | ||
| 287 | /// The user-provided buildBestIndex functions sets the index identifier. | ||
| 288 | /// These fields are meaningless for SQLite so they can be whatever you want as long as | ||
| 289 | /// both buildBestIndex and filter functions agree on what they mean. | ||
| 290 | pub const IndexIdentifier = struct { | ||
| 291 | num: i32 = 0, | ||
| 292 | str: []const u8 = "", | ||
| 293 | |||
| 294 | fn fromC(idx_num: c_int, idx_str: [*c]const u8) IndexIdentifier { | ||
| 295 | return IndexIdentifier{ | ||
| 296 | .num = @intCast(i32, idx_num), | ||
| 297 | .str = if (idx_str != null) mem.sliceTo(idx_str, 0) else "", | ||
| 298 | }; | ||
| 299 | } | ||
| 300 | }; | ||
| 301 | |||
| 302 | pub const FilterArg = struct { | ||
| 303 | value: ?*c.sqlite3_value, | ||
| 304 | |||
| 305 | pub fn as(self: FilterArg, comptime Type: type) Type { | ||
| 306 | var result: Type = undefined; | ||
| 307 | helpers.setTypeFromValue(Type, &result, self.value.?); | ||
| 308 | |||
| 309 | return result; | ||
| 310 | } | ||
| 311 | }; | ||
| 312 | |||
| 313 | /// Validates that a type implements everything required to be a cursor for a virtual table. | ||
| 314 | fn validateCursorType(comptime Table: type) void { | ||
| 315 | const Cursor = Table.Cursor; | ||
| 316 | |||
| 317 | // Validate the `init` function | ||
| 318 | { | ||
| 319 | if (!meta.trait.hasDecls(Cursor, .{"InitError"})) { | ||
| 320 | @compileError("the Cursor type must declare a InitError error set for the init function"); | ||
| 321 | } | ||
| 322 | |||
| 323 | const error_message = | ||
| 324 | \\the Cursor.init function must have the signature `fn init(allocator: std.mem.Allocator, parent: *Table) InitError!*Cursor` | ||
| 325 | ; | ||
| 326 | |||
| 327 | if (!meta.trait.hasFn("init")(Cursor)) { | ||
| 328 | @compileError("the Cursor type must have an init function, " ++ error_message); | ||
| 329 | } | ||
| 330 | |||
| 331 | const info = @typeInfo(@TypeOf(Cursor.init)).Fn; | ||
| 332 | |||
| 333 | if (info.args.len != 2) @compileError(error_message); | ||
| 334 | if (info.args[0].arg_type.? != mem.Allocator) @compileError(error_message); | ||
| 335 | if (info.args[1].arg_type.? != *Table) @compileError(error_message); | ||
| 336 | if (info.return_type.? != Cursor.InitError!*Cursor) @compileError(error_message); | ||
| 337 | } | ||
| 338 | |||
| 339 | // Validate the `deinit` function | ||
| 340 | { | ||
| 341 | const error_message = | ||
| 342 | \\the Cursor.deinit function must have the signature `fn deinit(cursor: *Cursor) void` | ||
| 343 | ; | ||
| 344 | |||
| 345 | if (!meta.trait.hasFn("deinit")(Cursor)) { | ||
| 346 | @compileError("the Cursor type must have a deinit function, " ++ error_message); | ||
| 347 | } | ||
| 348 | |||
| 349 | const info = @typeInfo(@TypeOf(Cursor.deinit)).Fn; | ||
| 350 | |||
| 351 | if (info.args.len != 1) @compileError(error_message); | ||
| 352 | if (info.args[0].arg_type.? != *Cursor) @compileError(error_message); | ||
| 353 | if (info.return_type.? != void) @compileError(error_message); | ||
| 354 | } | ||
| 355 | |||
| 356 | // Validate the `next` function | ||
| 357 | { | ||
| 358 | if (!meta.trait.hasDecls(Cursor, .{"NextError"})) { | ||
| 359 | @compileError("the Cursor type must declare a NextError error set for the next function"); | ||
| 360 | } | ||
| 361 | |||
| 362 | const error_message = | ||
| 363 | \\the Cursor.next function must have the signature `fn next(cursor: *Cursor, diags: *sqlite.vtab.VTabDiagnostics) NextError!void` | ||
| 364 | ; | ||
| 365 | |||
| 366 | if (!meta.trait.hasFn("next")(Cursor)) { | ||
| 367 | @compileError("the Cursor type must have a next function, " ++ error_message); | ||
| 368 | } | ||
| 369 | |||
| 370 | const info = @typeInfo(@TypeOf(Cursor.next)).Fn; | ||
| 371 | |||
| 372 | if (info.args.len != 2) @compileError(error_message); | ||
| 373 | if (info.args[0].arg_type.? != *Cursor) @compileError(error_message); | ||
| 374 | if (info.args[1].arg_type.? != *VTabDiagnostics) @compileError(error_message); | ||
| 375 | if (info.return_type.? != Cursor.NextError!void) @compileError(error_message); | ||
| 376 | } | ||
| 377 | |||
| 378 | // Validate the `hasNext` function | ||
| 379 | { | ||
| 380 | if (!meta.trait.hasDecls(Cursor, .{"HasNextError"})) { | ||
| 381 | @compileError("the Cursor type must declare a HasNextError error set for the hasNext function"); | ||
| 382 | } | ||
| 383 | |||
| 384 | const error_message = | ||
| 385 | \\the Cursor.hasNext function must have the signature `fn hasNext(cursor: *Cursor, diags: *sqlite.vtab.VTabDiagnostics) HasNextError!bool` | ||
| 386 | ; | ||
| 387 | |||
| 388 | if (!meta.trait.hasFn("hasNext")(Cursor)) { | ||
| 389 | @compileError("the Cursor type must have a hasNext function, " ++ error_message); | ||
| 390 | } | ||
| 391 | |||
| 392 | const info = @typeInfo(@TypeOf(Cursor.hasNext)).Fn; | ||
| 393 | |||
| 394 | if (info.args.len != 2) @compileError(error_message); | ||
| 395 | if (info.args[0].arg_type.? != *Cursor) @compileError(error_message); | ||
| 396 | if (info.args[1].arg_type.? != *VTabDiagnostics) @compileError(error_message); | ||
| 397 | if (info.return_type.? != Cursor.HasNextError!bool) @compileError(error_message); | ||
| 398 | } | ||
| 399 | |||
| 400 | // Validate the `filter` function | ||
| 401 | { | ||
| 402 | if (!meta.trait.hasDecls(Cursor, .{"FilterError"})) { | ||
| 403 | @compileError("the Cursor type must declare a FilterError error set for the filter function"); | ||
| 404 | } | ||
| 405 | |||
| 406 | const error_message = | ||
| 407 | \\the Cursor.filter function must have the signature `fn filter(cursor: *Cursor, diags: *sqlite.vtab.VTabDiagnostics, index: sqlite.vtab.IndexIdentifier, args: []FilterArg) FilterError!bool` | ||
| 408 | ; | ||
| 409 | |||
| 410 | if (!meta.trait.hasFn("filter")(Cursor)) { | ||
| 411 | @compileError("the Cursor type must have a filter function, " ++ error_message); | ||
| 412 | } | ||
| 413 | |||
| 414 | const info = @typeInfo(@TypeOf(Cursor.filter)).Fn; | ||
| 415 | |||
| 416 | if (info.args.len != 4) @compileError(error_message); | ||
| 417 | if (info.args[0].arg_type.? != *Cursor) @compileError(error_message); | ||
| 418 | if (info.args[1].arg_type.? != *VTabDiagnostics) @compileError(error_message); | ||
| 419 | if (info.args[2].arg_type.? != IndexIdentifier) @compileError(error_message); | ||
| 420 | if (info.args[3].arg_type.? != []FilterArg) @compileError(error_message); | ||
| 421 | if (info.return_type.? != Cursor.FilterError!void) @compileError(error_message); | ||
| 422 | } | ||
| 423 | |||
| 424 | // Validate the `column` function | ||
| 425 | { | ||
| 426 | if (!meta.trait.hasDecls(Cursor, .{"ColumnError"})) { | ||
| 427 | @compileError("the Cursor type must declare a ColumnError error set for the column function"); | ||
| 428 | } | ||
| 429 | if (!meta.trait.hasDecls(Cursor, .{"Column"})) { | ||
| 430 | @compileError("the Cursor type must declare a Column type for the return type of the column function"); | ||
| 431 | } | ||
| 432 | |||
| 433 | const error_message = | ||
| 434 | \\the Cursor.column function must have the signature `fn column(cursor: *Cursor, diags: *sqlite.vtab.VTabDiagnostics, column_number: i32) ColumnError!Column` | ||
| 435 | ; | ||
| 436 | |||
| 437 | if (!meta.trait.hasFn("column")(Cursor)) { | ||
| 438 | @compileError("the Cursor type must have a column function, " ++ error_message); | ||
| 439 | } | ||
| 440 | |||
| 441 | const info = @typeInfo(@TypeOf(Cursor.column)).Fn; | ||
| 442 | |||
| 443 | if (info.args.len != 3) @compileError(error_message); | ||
| 444 | if (info.args[0].arg_type.? != *Cursor) @compileError(error_message); | ||
| 445 | if (info.args[1].arg_type.? != *VTabDiagnostics) @compileError(error_message); | ||
| 446 | if (info.args[2].arg_type.? != i32) @compileError(error_message); | ||
| 447 | if (info.return_type.? != Cursor.ColumnError!Cursor.Column) @compileError(error_message); | ||
| 448 | } | ||
| 449 | |||
| 450 | // Validate the `rowId` function | ||
| 451 | { | ||
| 452 | if (!meta.trait.hasDecls(Cursor, .{"RowIDError"})) { | ||
| 453 | @compileError("the Cursor type must declare a RowIDError error set for the rowId function"); | ||
| 454 | } | ||
| 455 | |||
| 456 | const error_message = | ||
| 457 | \\the Cursor.rowId function must have the signature `fn rowId(cursor: *Cursor, diags: *sqlite.vtab.VTabDiagnostics) RowIDError!i64` | ||
| 458 | ; | ||
| 459 | |||
| 460 | if (!meta.trait.hasFn("rowId")(Cursor)) { | ||
| 461 | @compileError("the Cursor type must have a rowId function, " ++ error_message); | ||
| 462 | } | ||
| 463 | |||
| 464 | const info = @typeInfo(@TypeOf(Cursor.rowId)).Fn; | ||
| 465 | |||
| 466 | if (info.args.len != 2) @compileError(error_message); | ||
| 467 | if (info.args[0].arg_type.? != *Cursor) @compileError(error_message); | ||
| 468 | if (info.args[1].arg_type.? != *VTabDiagnostics) @compileError(error_message); | ||
| 469 | if (info.return_type.? != Cursor.RowIDError!i64) @compileError(error_message); | ||
| 470 | } | ||
| 471 | } | ||
| 472 | |||
| 473 | /// Validates that a type implements everything required to be a virtual table. | ||
| 474 | fn validateTableType(comptime Table: type) void { | ||
| 475 | // Validate the `init` function | ||
| 476 | { | ||
| 477 | if (!meta.trait.hasDecls(Table, .{"InitError"})) { | ||
| 478 | @compileError("the Table type must declare a InitError error set for the init function"); | ||
| 479 | } | ||
| 480 | |||
| 481 | const error_message = | ||
| 482 | \\the Table.init function must have the signature `fn init(allocator: std.mem.Allocator, diags: *sqlite.vtab.VTabDiagnostics) InitError!*Table` | ||
| 483 | ; | ||
| 484 | |||
| 485 | if (!meta.trait.hasFn("init")(Table)) { | ||
| 486 | @compileError("the Table type must have a init function, " ++ error_message); | ||
| 487 | } | ||
| 488 | |||
| 489 | const info = @typeInfo(@TypeOf(Table.init)).Fn; | ||
| 490 | |||
| 491 | if (info.args.len != 3) @compileError(error_message); | ||
| 492 | if (info.args[0].arg_type.? != mem.Allocator) @compileError(error_message); | ||
| 493 | if (info.args[1].arg_type.? != *VTabDiagnostics) @compileError(error_message); | ||
| 494 | // TODO(vincent): maybe allow a signature without the args since a table can do withoout them | ||
| 495 | if (info.args[2].arg_type.? != []const []const u8) @compileError(error_message); | ||
| 496 | if (info.return_type.? != Table.InitError!*Table) @compileError(error_message); | ||
| 497 | } | ||
| 498 | |||
| 499 | // Validate the `deinit` function | ||
| 500 | { | ||
| 501 | const error_message = | ||
| 502 | \\the Table.deinit function must have the signature `fn deinit(table: *Table, allocator: std.mem.Allocator) void` | ||
| 503 | ; | ||
| 504 | |||
| 505 | if (!meta.trait.hasFn("deinit")(Table)) { | ||
| 506 | @compileError("the Table type must have a deinit function, " ++ error_message); | ||
| 507 | } | ||
| 508 | |||
| 509 | const info = @typeInfo(@TypeOf(Table.deinit)).Fn; | ||
| 510 | |||
| 511 | if (info.args.len != 2) @compileError(error_message); | ||
| 512 | if (info.args[0].arg_type.? != *Table) @compileError(error_message); | ||
| 513 | if (info.args[1].arg_type.? != mem.Allocator) @compileError(error_message); | ||
| 514 | if (info.return_type.? != void) @compileError(error_message); | ||
| 515 | } | ||
| 516 | |||
| 517 | // Validate the `buildBestIndex` function | ||
| 518 | { | ||
| 519 | if (!meta.trait.hasDecls(Table, .{"BuildBestIndexError"})) { | ||
| 520 | @compileError("the Cursor type must declare a BuildBestIndexError error set for the buildBestIndex function"); | ||
| 521 | } | ||
| 522 | |||
| 523 | const error_message = | ||
| 524 | \\the Table.buildBestIndex function must have the signature `fn buildBestIndex(table: *Table, diags: *sqlite.vtab.VTabDiagnostics, builder: *sqlite.vtab.BestIndexBuilder) BuildBestIndexError!void` | ||
| 525 | ; | ||
| 526 | |||
| 527 | if (!meta.trait.hasFn("buildBestIndex")(Table)) { | ||
| 528 | @compileError("the Table type must have a buildBestIndex function, " ++ error_message); | ||
| 529 | } | ||
| 530 | |||
| 531 | const info = @typeInfo(@TypeOf(Table.buildBestIndex)).Fn; | ||
| 532 | |||
| 533 | if (info.args.len != 3) @compileError(error_message); | ||
| 534 | if (info.args[0].arg_type.? != *Table) @compileError(error_message); | ||
| 535 | if (info.args[1].arg_type.? != *VTabDiagnostics) @compileError(error_message); | ||
| 536 | if (info.args[2].arg_type.? != *BestIndexBuilder) @compileError(error_message); | ||
| 537 | if (info.return_type.? != Table.BuildBestIndexError!void) @compileError(error_message); | ||
| 538 | } | ||
| 539 | |||
| 540 | if (!meta.trait.hasDecls(Table, .{"Cursor"})) { | ||
| 541 | @compileError("the Table type must declare a Cursor type"); | ||
| 542 | } | ||
| 543 | } | ||
| 544 | |||
| 545 | pub fn VirtualTable( | ||
| 546 | comptime table_name: [:0]const u8, | ||
| 547 | comptime Table: type, | ||
| 548 | ) type { | ||
| 549 | // Validate the Table type | ||
| 550 | |||
| 551 | comptime { | ||
| 552 | validateTableType(Table); | ||
| 553 | validateCursorType(Table); | ||
| 554 | } | ||
| 555 | |||
| 556 | const State = struct { | ||
| 557 | const Self = @This(); | ||
| 558 | |||
| 559 | /// vtab must come first ! | ||
| 560 | /// The different functions receive a pointer to a vtab so we have to use @fieldParentPtr to get our state. | ||
| 561 | vtab: c.sqlite3_vtab, | ||
| 562 | /// The module context contains state that's the same for _all_ implementations of virtual tables. | ||
| 563 | module_context: *ModuleContext, | ||
| 564 | /// The table is the actual virtual table implementation. | ||
| 565 | table: *Table, | ||
| 566 | |||
| 567 | const InitError = error{} || mem.Allocator.Error || Table.InitError; | ||
| 568 | |||
| 569 | fn init(module_context: *ModuleContext, table: *Table) InitError!*Self { | ||
| 570 | var res = try module_context.allocator.create(Self); | ||
| 571 | res.* = .{ | ||
| 572 | .vtab = mem.zeroes(c.sqlite3_vtab), | ||
| 573 | .module_context = module_context, | ||
| 574 | .table = table, | ||
| 575 | }; | ||
| 576 | return res; | ||
| 577 | } | ||
| 578 | |||
| 579 | fn deinit(self: *Self) void { | ||
| 580 | self.table.deinit(self.module_context.allocator); | ||
| 581 | self.module_context.allocator.destroy(self); | ||
| 582 | } | ||
| 583 | }; | ||
| 584 | |||
| 585 | const CursorState = struct { | ||
| 586 | const Self = @This(); | ||
| 587 | |||
| 588 | /// vtab_cursor must come first ! | ||
| 589 | /// The different functions receive a pointer to a vtab_cursor so we have to use @fieldParentPtr to get our state. | ||
| 590 | vtab_cursor: c.sqlite3_vtab_cursor, | ||
| 591 | /// The module context contains state that's the same for _all_ implementations of virtual tables. | ||
| 592 | module_context: *ModuleContext, | ||
| 593 | /// The table is the actual virtual table implementation. | ||
| 594 | table: *Table, | ||
| 595 | cursor: *Table.Cursor, | ||
| 596 | |||
| 597 | const InitError = error{} || mem.Allocator.Error || Table.Cursor.InitError; | ||
| 598 | |||
| 599 | fn init(module_context: *ModuleContext, table: *Table) InitError!*Self { | ||
| 600 | var res = try module_context.allocator.create(Self); | ||
| 601 | errdefer module_context.allocator.destroy(res); | ||
| 602 | |||
| 603 | res.* = .{ | ||
| 604 | .vtab_cursor = mem.zeroes(c.sqlite3_vtab_cursor), | ||
| 605 | .module_context = module_context, | ||
| 606 | .table = table, | ||
| 607 | .cursor = try Table.Cursor.init(module_context.allocator, table), | ||
| 608 | }; | ||
| 609 | |||
| 610 | return res; | ||
| 611 | } | ||
| 612 | |||
| 613 | fn deinit(self: *Self) void { | ||
| 614 | self.cursor.deinit(); | ||
| 615 | self.module_context.allocator.destroy(self); | ||
| 616 | } | ||
| 617 | }; | ||
| 618 | |||
| 619 | return struct { | ||
| 620 | const Self = @This(); | ||
| 621 | |||
| 622 | pub const name = table_name; | ||
| 623 | pub const module = if (versionGreaterThanOrEqualTo(3, 26, 0)) | ||
| 624 | c.sqlite3_module{ | ||
| 625 | .iVersion = 0, | ||
| 626 | .xCreate = xConnect, // TODO(vincent): implement xCreate and use it | ||
| 627 | .xConnect = xConnect, | ||
| 628 | .xBestIndex = xBestIndex, | ||
| 629 | .xDisconnect = xDisconnect, | ||
| 630 | .xDestroy = xDisconnect, // TODO(vincent): implement xDestroy and use it | ||
| 631 | .xOpen = xOpen, | ||
| 632 | .xClose = xClose, | ||
| 633 | .xFilter = xFilter, | ||
| 634 | .xNext = xNext, | ||
| 635 | .xEof = xEof, | ||
| 636 | .xColumn = xColumn, | ||
| 637 | .xRowid = xRowid, | ||
| 638 | .xUpdate = null, | ||
| 639 | .xBegin = null, | ||
| 640 | .xSync = null, | ||
| 641 | .xCommit = null, | ||
| 642 | .xRollback = null, | ||
| 643 | .xFindFunction = null, | ||
| 644 | .xRename = null, | ||
| 645 | .xSavepoint = null, | ||
| 646 | .xRelease = null, | ||
| 647 | .xRollbackTo = null, | ||
| 648 | .xShadowName = null, | ||
| 649 | } | ||
| 650 | else | ||
| 651 | c.sqlite3_module{ | ||
| 652 | .iVersion = 0, | ||
| 653 | .xCreate = xConnect, // TODO(vincent): implement xCreate and use it | ||
| 654 | .xConnect = xConnect, | ||
| 655 | .xBestIndex = xBestIndex, | ||
| 656 | .xDisconnect = xDisconnect, | ||
| 657 | .xDestroy = xDisconnect, // TODO(vincent): implement xDestroy and use it | ||
| 658 | .xOpen = xOpen, | ||
| 659 | .xClose = xClose, | ||
| 660 | .xFilter = xFilter, | ||
| 661 | .xNext = xNext, | ||
| 662 | .xEof = xEof, | ||
| 663 | .xColumn = xColumn, | ||
| 664 | .xRowid = xRowid, | ||
| 665 | .xUpdate = null, | ||
| 666 | .xBegin = null, | ||
| 667 | .xSync = null, | ||
| 668 | .xCommit = null, | ||
| 669 | .xRollback = null, | ||
| 670 | .xFindFunction = null, | ||
| 671 | .xRename = null, | ||
| 672 | .xSavepoint = null, | ||
| 673 | .xRelease = null, | ||
| 674 | .xRollbackTo = null, | ||
| 675 | }; | ||
| 676 | |||
| 677 | table: Table, | ||
| 678 | |||
| 679 | fn getModuleContext(ptr: ?*anyopaque) *ModuleContext { | ||
| 680 | return @ptrCast(*ModuleContext, @alignCast(@alignOf(ModuleContext), ptr.?)); | ||
| 681 | } | ||
| 682 | |||
| 683 | fn createState(allocator: mem.Allocator, diags: *VTabDiagnostics, module_context: *ModuleContext, args: []const []const u8) !*State { | ||
| 684 | // The Context holds the complete of the virtual table and lives for its entire lifetime. | ||
| 685 | // Context.deinit() will be called when xDestroy is called. | ||
| 686 | |||
| 687 | var table = try Table.init(allocator, diags, args); | ||
| 688 | errdefer table.deinit(allocator); | ||
| 689 | |||
| 690 | return try State.init(module_context, table); | ||
| 691 | } | ||
| 692 | |||
| 693 | 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 { | ||
| 694 | _ = db; | ||
| 695 | _ = module_context_ptr; | ||
| 696 | _ = argc; | ||
| 697 | _ = argv; | ||
| 698 | _ = vtab; | ||
| 699 | _ = err_str; | ||
| 700 | |||
| 701 | debug.print("xCreate\n", .{}); | ||
| 702 | |||
| 703 | return c.SQLITE_ERROR; | ||
| 704 | } | ||
| 705 | |||
| 706 | 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 { | ||
| 707 | const module_context = getModuleContext(module_context_ptr); | ||
| 708 | |||
| 709 | var arena = heap.ArenaAllocator.init(module_context.allocator); | ||
| 710 | defer arena.deinit(); | ||
| 711 | |||
| 712 | // Convert the C-like args to more idiomatic types. | ||
| 713 | // TODO(vincent): maybe we should provide a way to automatically parse arguments | ||
| 714 | const args = stringSliceFromCPointer(arena.allocator(), argc, argv) catch { | ||
| 715 | err_str.* = dupeToSQLiteString("out of memory"); | ||
| 716 | return c.SQLITE_ERROR; | ||
| 717 | }; | ||
| 718 | |||
| 719 | // | ||
| 720 | // Create the context and state, assign it to the vtab and declare the vtab. | ||
| 721 | // | ||
| 722 | |||
| 723 | var diags = VTabDiagnostics{ .allocator = arena.allocator() }; | ||
| 724 | const state = createState(module_context.allocator, &diags, module_context, args) catch { | ||
| 725 | err_str.* = dupeToSQLiteString(diags.error_message); | ||
| 726 | return c.SQLITE_ERROR; | ||
| 727 | }; | ||
| 728 | vtab.* = @ptrCast(*c.sqlite3_vtab, state); | ||
| 729 | |||
| 730 | const res = c.sqlite3_declare_vtab(db, @ptrCast([*c]const u8, state.table.schema)); | ||
| 731 | if (res != c.SQLITE_OK) { | ||
| 732 | return c.SQLITE_ERROR; | ||
| 733 | } | ||
| 734 | |||
| 735 | return c.SQLITE_OK; | ||
| 736 | } | ||
| 737 | |||
| 738 | fn xBestIndex(vtab: [*c]c.sqlite3_vtab, index_info_ptr: [*c]c.sqlite3_index_info) callconv(.C) c_int { | ||
| 739 | const index_info: *c.sqlite3_index_info = index_info_ptr orelse unreachable; | ||
| 740 | |||
| 741 | // | ||
| 742 | |||
| 743 | const state = @fieldParentPtr(State, "vtab", vtab); | ||
| 744 | |||
| 745 | var arena = heap.ArenaAllocator.init(state.module_context.allocator); | ||
| 746 | defer arena.deinit(); | ||
| 747 | |||
| 748 | // Create an index builder and let the user build the index. | ||
| 749 | |||
| 750 | var builder = BestIndexBuilder.init(arena.allocator(), index_info) catch |err| { | ||
| 751 | logger.err("unable to create best index builder, err: {!}", .{err}); | ||
| 752 | return c.SQLITE_ERROR; | ||
| 753 | }; | ||
| 754 | |||
| 755 | var diags = VTabDiagnostics{ .allocator = arena.allocator() }; | ||
| 756 | state.table.buildBestIndex(&diags, &builder) catch |err| { | ||
| 757 | logger.err("unable to build best index, err: {!}", .{err}); | ||
| 758 | return c.SQLITE_ERROR; | ||
| 759 | }; | ||
| 760 | |||
| 761 | return c.SQLITE_OK; | ||
| 762 | } | ||
| 763 | |||
| 764 | fn xDisconnect(vtab: [*c]c.sqlite3_vtab) callconv(.C) c_int { | ||
| 765 | const state = @fieldParentPtr(State, "vtab", vtab); | ||
| 766 | state.deinit(); | ||
| 767 | |||
| 768 | return c.SQLITE_OK; | ||
| 769 | } | ||
| 770 | |||
| 771 | fn xDestroy(vtab: [*c]c.sqlite3_vtab) callconv(.C) c_int { | ||
| 772 | _ = vtab; | ||
| 773 | |||
| 774 | debug.print("xDestroy\n", .{}); | ||
| 775 | |||
| 776 | return c.SQLITE_ERROR; | ||
| 777 | } | ||
| 778 | |||
| 779 | fn xOpen(vtab: [*c]c.sqlite3_vtab, vtab_cursor: [*c][*c]c.sqlite3_vtab_cursor) callconv(.C) c_int { | ||
| 780 | const state = @fieldParentPtr(State, "vtab", vtab); | ||
| 781 | |||
| 782 | const cursor_state = CursorState.init(state.module_context, state.table) catch |err| { | ||
| 783 | logger.err("unable to create cursor state, err: {!}", .{err}); | ||
| 784 | return c.SQLITE_ERROR; | ||
| 785 | }; | ||
| 786 | vtab_cursor.* = @ptrCast(*c.sqlite3_vtab_cursor, cursor_state); | ||
| 787 | |||
| 788 | return c.SQLITE_OK; | ||
| 789 | } | ||
| 790 | |||
| 791 | fn xClose(vtab_cursor: [*c]c.sqlite3_vtab_cursor) callconv(.C) c_int { | ||
| 792 | const cursor_state = @fieldParentPtr(CursorState, "vtab_cursor", vtab_cursor); | ||
| 793 | cursor_state.deinit(); | ||
| 794 | |||
| 795 | return c.SQLITE_OK; | ||
| 796 | } | ||
| 797 | |||
| 798 | fn xEof(vtab_cursor: [*c]c.sqlite3_vtab_cursor) callconv(.C) c_int { | ||
| 799 | const cursor_state = @fieldParentPtr(CursorState, "vtab_cursor", vtab_cursor); | ||
| 800 | const cursor = cursor_state.cursor; | ||
| 801 | |||
| 802 | var arena = heap.ArenaAllocator.init(cursor_state.module_context.allocator); | ||
| 803 | defer arena.deinit(); | ||
| 804 | |||
| 805 | // | ||
| 806 | |||
| 807 | var diags = VTabDiagnostics{ .allocator = arena.allocator() }; | ||
| 808 | const has_next = cursor.hasNext(&diags) catch { | ||
| 809 | logger.err("unable to call Table.Cursor.hasNext: {s}", .{diags.error_message}); | ||
| 810 | return 1; | ||
| 811 | }; | ||
| 812 | |||
| 813 | if (has_next) { | ||
| 814 | return 0; | ||
| 815 | } else { | ||
| 816 | return 1; | ||
| 817 | } | ||
| 818 | } | ||
| 819 | |||
| 820 | const FilterArgsFromCPointerError = error{} || mem.Allocator.Error; | ||
| 821 | |||
| 822 | fn filterArgsFromCPointer(allocator: mem.Allocator, argc: c_int, argv: [*c]?*c.sqlite3_value) FilterArgsFromCPointerError![]FilterArg { | ||
| 823 | const size = @intCast(usize, argc); | ||
| 824 | |||
| 825 | var res = try allocator.alloc(FilterArg, size); | ||
| 826 | for (res) |*item, i| { | ||
| 827 | item.* = .{ | ||
| 828 | .value = argv[i], | ||
| 829 | }; | ||
| 830 | } | ||
| 831 | |||
| 832 | return res; | ||
| 833 | } | ||
| 834 | |||
| 835 | 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 { | ||
| 836 | const cursor_state = @fieldParentPtr(CursorState, "vtab_cursor", vtab_cursor); | ||
| 837 | const cursor = cursor_state.cursor; | ||
| 838 | |||
| 839 | var arena = heap.ArenaAllocator.init(cursor_state.module_context.allocator); | ||
| 840 | defer arena.deinit(); | ||
| 841 | |||
| 842 | // | ||
| 843 | |||
| 844 | const id = IndexIdentifier.fromC(idx_num, idx_str); | ||
| 845 | |||
| 846 | var args = filterArgsFromCPointer(arena.allocator(), argc, argv) catch |err| { | ||
| 847 | logger.err("unable to create filter args, err: {!}", .{err}); | ||
| 848 | return c.SQLITE_ERROR; | ||
| 849 | }; | ||
| 850 | |||
| 851 | var diags = VTabDiagnostics{ .allocator = arena.allocator() }; | ||
| 852 | cursor.filter(&diags, id, args) catch { | ||
| 853 | logger.err("unable to call Table.Cursor.filter: {s}", .{diags.error_message}); | ||
| 854 | return c.SQLITE_ERROR; | ||
| 855 | }; | ||
| 856 | |||
| 857 | return c.SQLITE_OK; | ||
| 858 | } | ||
| 859 | |||
| 860 | fn xNext(vtab_cursor: [*c]c.sqlite3_vtab_cursor) callconv(.C) c_int { | ||
| 861 | const cursor_state = @fieldParentPtr(CursorState, "vtab_cursor", vtab_cursor); | ||
| 862 | const cursor = cursor_state.cursor; | ||
| 863 | |||
| 864 | var arena = heap.ArenaAllocator.init(cursor_state.module_context.allocator); | ||
| 865 | defer arena.deinit(); | ||
| 866 | |||
| 867 | // | ||
| 868 | |||
| 869 | var diags = VTabDiagnostics{ .allocator = arena.allocator() }; | ||
| 870 | cursor.next(&diags) catch { | ||
| 871 | logger.err("unable to call Table.Cursor.next: {s}", .{diags.error_message}); | ||
| 872 | return c.SQLITE_ERROR; | ||
| 873 | }; | ||
| 874 | |||
| 875 | return c.SQLITE_OK; | ||
| 876 | } | ||
| 877 | |||
| 878 | fn xColumn(vtab_cursor: [*c]c.sqlite3_vtab_cursor, ctx: ?*c.sqlite3_context, n: c_int) callconv(.C) c_int { | ||
| 879 | const cursor_state = @fieldParentPtr(CursorState, "vtab_cursor", vtab_cursor); | ||
| 880 | const cursor = cursor_state.cursor; | ||
| 881 | |||
| 882 | var arena = heap.ArenaAllocator.init(cursor_state.module_context.allocator); | ||
| 883 | defer arena.deinit(); | ||
| 884 | |||
| 885 | // | ||
| 886 | |||
| 887 | var diags = VTabDiagnostics{ .allocator = arena.allocator() }; | ||
| 888 | const column = cursor.column(&diags, @intCast(i32, n)) catch { | ||
| 889 | logger.err("unable to call Table.Cursor.column: {s}", .{diags.error_message}); | ||
| 890 | return c.SQLITE_ERROR; | ||
| 891 | }; | ||
| 892 | |||
| 893 | // TODO(vincent): does it make sense to put this in setResult ? Functions could also return a union. | ||
| 894 | const ColumnType = @TypeOf(column); | ||
| 895 | switch (@typeInfo(ColumnType)) { | ||
| 896 | .Union => |info| { | ||
| 897 | if (info.tag_type) |UnionTagType| { | ||
| 898 | inline for (info.fields) |u_field| { | ||
| 899 | |||
| 900 | // This wasn't entirely obvious when I saw code like this elsewhere, it works because of type coercion. | ||
| 901 | // See https://ziglang.org/documentation/master/#Type-Coercion-unions-and-enums | ||
| 902 | const column_tag: std.meta.Tag(ColumnType) = column; | ||
| 903 | const this_tag: std.meta.Tag(ColumnType) = @field(UnionTagType, u_field.name); | ||
| 904 | |||
| 905 | if (column_tag == this_tag) { | ||
| 906 | const column_value = @field(column, u_field.name); | ||
| 907 | |||
| 908 | helpers.setResult(ctx, column_value); | ||
| 909 | } | ||
| 910 | } | ||
| 911 | } else { | ||
| 912 | @compileError("cannot use bare unions as a column"); | ||
| 913 | } | ||
| 914 | }, | ||
| 915 | else => helpers.setResult(ctx, column), | ||
| 916 | } | ||
| 917 | |||
| 918 | return c.SQLITE_OK; | ||
| 919 | } | ||
| 920 | |||
| 921 | fn xRowid(vtab_cursor: [*c]c.sqlite3_vtab_cursor, row_id_ptr: [*c]c.sqlite3_int64) callconv(.C) c_int { | ||
| 922 | const cursor_state = @fieldParentPtr(CursorState, "vtab_cursor", vtab_cursor); | ||
| 923 | const cursor = cursor_state.cursor; | ||
| 924 | |||
| 925 | var arena = heap.ArenaAllocator.init(cursor_state.module_context.allocator); | ||
| 926 | defer arena.deinit(); | ||
| 927 | |||
| 928 | // | ||
| 929 | |||
| 930 | var diags = VTabDiagnostics{ .allocator = arena.allocator() }; | ||
| 931 | const row_id = cursor.rowId(&diags) catch { | ||
| 932 | logger.err("unable to call Table.Cursor.rowId: {s}", .{diags.error_message}); | ||
| 933 | return c.SQLITE_ERROR; | ||
| 934 | }; | ||
| 935 | |||
| 936 | row_id_ptr.* = row_id; | ||
| 937 | |||
| 938 | return c.SQLITE_OK; | ||
| 939 | } | ||
| 940 | }; | ||
| 941 | } | ||
| 942 | |||
| 943 | const TestVirtualTable = struct { | ||
| 944 | pub const Cursor = TestVirtualTableCursor; | ||
| 945 | |||
| 946 | const Row = struct { | ||
| 947 | foo: []const u8, | ||
| 948 | bar: []const u8, | ||
| 949 | baz: isize, | ||
| 950 | }; | ||
| 951 | |||
| 952 | arena_state: heap.ArenaAllocator.State, | ||
| 953 | |||
| 954 | rows: []Row, | ||
| 955 | schema: [:0]const u8, | ||
| 956 | |||
| 957 | pub const InitError = error{} || mem.Allocator.Error || fmt.ParseIntError; | ||
| 958 | |||
| 959 | pub fn init(gpa: mem.Allocator, diags: *VTabDiagnostics, args: []const []const u8) InitError!*TestVirtualTable { | ||
| 960 | var arena = heap.ArenaAllocator.init(gpa); | ||
| 961 | const allocator = arena.allocator(); | ||
| 962 | |||
| 963 | var res = try allocator.create(TestVirtualTable); | ||
| 964 | errdefer res.deinit(gpa); | ||
| 965 | |||
| 966 | // Generate test data | ||
| 967 | // TODO(vincent): maybe we should provide a way to automatically parse arguments | ||
| 968 | const rows = blk: { | ||
| 969 | var content: []const u8 = ""; | ||
| 970 | var n: usize = 0; | ||
| 971 | |||
| 972 | for (args) |arg| { | ||
| 973 | if (mem.startsWith(u8, arg, "content=")) { | ||
| 974 | const pos = mem.indexOfScalar(u8, arg, '=') orelse continue; | ||
| 975 | content = arg[pos + 1 ..]; | ||
| 976 | } else if (mem.startsWith(u8, arg, "n=")) { | ||
| 977 | const pos = mem.indexOfScalar(u8, arg, '=') orelse continue; | ||
| 978 | const value = arg[pos + 1 ..]; | ||
| 979 | n = fmt.parseInt(usize, value, 10) catch |err| { | ||
| 980 | switch (err) { | ||
| 981 | error.InvalidCharacter => diags.setErrorMessage("not a number: {s}", .{value}), | ||
| 982 | else => diags.setErrorMessage("got error while parsing value {s}: {!}", .{ value, err }), | ||
| 983 | } | ||
| 984 | return err; | ||
| 985 | }; | ||
| 986 | } | ||
| 987 | } | ||
| 988 | |||
| 989 | // TODO-vincent): stop using random data so we can actually test | ||
| 990 | const data = &[_][]const u8{ | ||
| 991 | "Vincent", "José", "Michel", | ||
| 992 | }; | ||
| 993 | |||
| 994 | var rand = std.rand.DefaultPrng.init(204882485); | ||
| 995 | |||
| 996 | var tmp = try allocator.alloc(Row, n); | ||
| 997 | for (tmp) |*s| { | ||
| 998 | const foo_value = data[rand.random().intRangeLessThan(usize, 0, data.len)]; | ||
| 999 | const bar_value = data[rand.random().intRangeLessThan(usize, 0, data.len)]; | ||
| 1000 | const baz_value = rand.random().intRangeAtMost(isize, 0, 200); | ||
| 1001 | |||
| 1002 | s.* = .{ | ||
| 1003 | .foo = foo_value, | ||
| 1004 | .bar = bar_value, | ||
| 1005 | .baz = baz_value, | ||
| 1006 | }; | ||
| 1007 | } | ||
| 1008 | |||
| 1009 | break :blk tmp; | ||
| 1010 | }; | ||
| 1011 | res.rows = rows; | ||
| 1012 | |||
| 1013 | // Build the schema | ||
| 1014 | res.schema = try allocator.dupeZ(u8, | ||
| 1015 | \\CREATE TABLE foobar(foo TEXT, bar TEXT, baz INTEGER) | ||
| 1016 | ); | ||
| 1017 | |||
| 1018 | res.arena_state = arena.state; | ||
| 1019 | |||
| 1020 | return res; | ||
| 1021 | } | ||
| 1022 | |||
| 1023 | pub fn deinit(self: *TestVirtualTable, gpa: mem.Allocator) void { | ||
| 1024 | self.arena_state.promote(gpa).deinit(); | ||
| 1025 | } | ||
| 1026 | |||
| 1027 | fn connect(self: *TestVirtualTable) anyerror!void { | ||
| 1028 | _ = self; | ||
| 1029 | debug.print("connect\n", .{}); | ||
| 1030 | } | ||
| 1031 | |||
| 1032 | pub const BuildBestIndexError = error{} || mem.Allocator.Error; | ||
| 1033 | |||
| 1034 | pub fn buildBestIndex(self: *TestVirtualTable, diags: *VTabDiagnostics, builder: *BestIndexBuilder) BuildBestIndexError!void { | ||
| 1035 | _ = self; | ||
| 1036 | _ = diags; | ||
| 1037 | |||
| 1038 | var id_str_writer = builder.id_str_buffer.writer(); | ||
| 1039 | |||
| 1040 | var argv_index: i32 = 0; | ||
| 1041 | for (builder.constraints) |*constraint| { | ||
| 1042 | if (constraint.op == .eq) { | ||
| 1043 | argv_index += 1; | ||
| 1044 | constraint.usage.argv_index = argv_index; | ||
| 1045 | |||
| 1046 | try id_str_writer.print("={d:<6}", .{constraint.column}); | ||
| 1047 | } | ||
| 1048 | } | ||
| 1049 | |||
| 1050 | // | ||
| 1051 | |||
| 1052 | builder.id.str = builder.id_str_buffer.toOwnedSlice(); | ||
| 1053 | builder.estimated_cost = 200; | ||
| 1054 | builder.estimated_rows = 200; | ||
| 1055 | |||
| 1056 | builder.build(); | ||
| 1057 | } | ||
| 1058 | |||
| 1059 | /// An iterator over the rows of this table capable of applying filters. | ||
| 1060 | /// The filters are used when the index asks for it. | ||
| 1061 | const Iterator = struct { | ||
| 1062 | rows: []Row, | ||
| 1063 | pos: usize, | ||
| 1064 | |||
| 1065 | filters: struct { | ||
| 1066 | foo: ?[]const u8 = null, | ||
| 1067 | bar: ?[]const u8 = null, | ||
| 1068 | } = .{}, | ||
| 1069 | |||
| 1070 | fn init(rows: []Row) Iterator { | ||
| 1071 | return Iterator{ | ||
| 1072 | .rows = rows, | ||
| 1073 | .pos = 0, | ||
| 1074 | }; | ||
| 1075 | } | ||
| 1076 | |||
| 1077 | fn currentRow(it: *Iterator) Row { | ||
| 1078 | return it.rows[it.pos]; | ||
| 1079 | } | ||
| 1080 | |||
| 1081 | fn hasNext(it: *Iterator) bool { | ||
| 1082 | return it.pos < it.rows.len; | ||
| 1083 | } | ||
| 1084 | |||
| 1085 | fn next(it: *Iterator) void { | ||
| 1086 | const foo = it.filters.foo orelse ""; | ||
| 1087 | const bar = it.filters.bar orelse ""; | ||
| 1088 | |||
| 1089 | it.pos += 1; | ||
| 1090 | |||
| 1091 | while (it.pos < it.rows.len) : (it.pos += 1) { | ||
| 1092 | const row = it.rows[it.pos]; | ||
| 1093 | |||
| 1094 | if (foo.len > 0 and bar.len > 0 and mem.eql(u8, foo, row.foo) and mem.eql(u8, bar, row.bar)) break; | ||
| 1095 | if (foo.len > 0 and mem.eql(u8, foo, row.foo)) break; | ||
| 1096 | if (bar.len > 0 and mem.eql(u8, bar, row.bar)) break; | ||
| 1097 | } | ||
| 1098 | } | ||
| 1099 | }; | ||
| 1100 | }; | ||
| 1101 | |||
| 1102 | const TestVirtualTableCursor = struct { | ||
| 1103 | allocator: mem.Allocator, | ||
| 1104 | parent: *TestVirtualTable, | ||
| 1105 | iterator: TestVirtualTable.Iterator, | ||
| 1106 | |||
| 1107 | pub const InitError = error{} || mem.Allocator.Error; | ||
| 1108 | |||
| 1109 | pub fn init(allocator: mem.Allocator, parent: *TestVirtualTable) InitError!*TestVirtualTableCursor { | ||
| 1110 | var res = try allocator.create(TestVirtualTableCursor); | ||
| 1111 | res.* = .{ | ||
| 1112 | .allocator = allocator, | ||
| 1113 | .parent = parent, | ||
| 1114 | .iterator = TestVirtualTable.Iterator.init(parent.rows), | ||
| 1115 | }; | ||
| 1116 | return res; | ||
| 1117 | } | ||
| 1118 | |||
| 1119 | pub fn deinit(cursor: *TestVirtualTableCursor) void { | ||
| 1120 | cursor.allocator.destroy(cursor); | ||
| 1121 | } | ||
| 1122 | |||
| 1123 | pub const FilterError = error{InvalidColumn} || fmt.ParseIntError; | ||
| 1124 | |||
| 1125 | pub fn filter(cursor: *TestVirtualTableCursor, diags: *VTabDiagnostics, index: IndexIdentifier, args: []FilterArg) FilterError!void { | ||
| 1126 | _ = cursor; | ||
| 1127 | _ = diags; | ||
| 1128 | _ = index; | ||
| 1129 | _ = args; | ||
| 1130 | |||
| 1131 | debug.print("idx num: {d}\n", .{index.num}); | ||
| 1132 | debug.print("idx str: {s}\n", .{index.str}); | ||
| 1133 | |||
| 1134 | var id = index.str; | ||
| 1135 | |||
| 1136 | // NOTE(vincent): this is an ugly ass parser for the index string, don't judge me. | ||
| 1137 | |||
| 1138 | var i: usize = 0; | ||
| 1139 | while (true) { | ||
| 1140 | const pos = mem.indexOfScalar(u8, id, '=') orelse break; | ||
| 1141 | |||
| 1142 | const arg = args[i]; | ||
| 1143 | i += 1; | ||
| 1144 | |||
| 1145 | // 3 chars for the '=' marker | ||
| 1146 | // 6 chars because we format all columns in a 6 char wide string | ||
| 1147 | const col_str = id[pos + 1 .. pos + 1 + 6]; | ||
| 1148 | const col = try fmt.parseInt(i32, mem.trimRight(u8, col_str, " "), 10); | ||
| 1149 | |||
| 1150 | id = id[pos + 1 + 6 ..]; | ||
| 1151 | |||
| 1152 | // | ||
| 1153 | |||
| 1154 | if (col == 0) { | ||
| 1155 | cursor.iterator.filters.foo = arg.as([]const u8); | ||
| 1156 | } else if (col == 1) { | ||
| 1157 | cursor.iterator.filters.bar = arg.as([]const u8); | ||
| 1158 | } else if (col == 2) { | ||
| 1159 | _ = arg.as(isize); | ||
| 1160 | } else { | ||
| 1161 | return error.InvalidColumn; | ||
| 1162 | } | ||
| 1163 | } | ||
| 1164 | |||
| 1165 | debug.print("expected args: {d}\n", .{i}); | ||
| 1166 | } | ||
| 1167 | |||
| 1168 | pub const NextError = error{}; | ||
| 1169 | |||
| 1170 | pub fn next(cursor: *TestVirtualTableCursor, diags: *VTabDiagnostics) NextError!void { | ||
| 1171 | _ = diags; | ||
| 1172 | |||
| 1173 | cursor.iterator.next(); | ||
| 1174 | } | ||
| 1175 | |||
| 1176 | pub const HasNextError = error{}; | ||
| 1177 | |||
| 1178 | pub fn hasNext(cursor: *TestVirtualTableCursor, diags: *VTabDiagnostics) HasNextError!bool { | ||
| 1179 | _ = diags; | ||
| 1180 | |||
| 1181 | return cursor.iterator.hasNext(); | ||
| 1182 | } | ||
| 1183 | |||
| 1184 | pub const Column = union(enum) { | ||
| 1185 | foo: []const u8, | ||
| 1186 | bar: []const u8, | ||
| 1187 | baz: isize, | ||
| 1188 | }; | ||
| 1189 | |||
| 1190 | pub const ColumnError = error{InvalidColumn}; | ||
| 1191 | |||
| 1192 | pub fn column(cursor: *TestVirtualTableCursor, diags: *VTabDiagnostics, column_number: i32) ColumnError!Column { | ||
| 1193 | _ = cursor; | ||
| 1194 | _ = diags; | ||
| 1195 | |||
| 1196 | const row = cursor.iterator.currentRow(); | ||
| 1197 | |||
| 1198 | switch (column_number) { | ||
| 1199 | 0 => return Column{ .foo = row.foo }, | ||
| 1200 | 1 => return Column{ .bar = row.bar }, | ||
| 1201 | 2 => return Column{ .baz = row.baz }, | ||
| 1202 | else => return error.InvalidColumn, | ||
| 1203 | } | ||
| 1204 | } | ||
| 1205 | |||
| 1206 | pub const RowIDError = error{}; | ||
| 1207 | |||
| 1208 | pub fn rowId(cursor: *TestVirtualTableCursor, diags: *VTabDiagnostics) RowIDError!i64 { | ||
| 1209 | _ = diags; | ||
| 1210 | |||
| 1211 | return @intCast(i64, cursor.iterator.pos); | ||
| 1212 | } | ||
| 1213 | }; | ||
| 1214 | |||
| 1215 | test "virtual table" { | ||
| 1216 | var db = try getTestDb(); | ||
| 1217 | defer db.deinit(); | ||
| 1218 | |||
| 1219 | var myvtab_module_context = ModuleContext{ | ||
| 1220 | .allocator = testing.allocator, | ||
| 1221 | }; | ||
| 1222 | |||
| 1223 | try db.createVirtualTable( | ||
| 1224 | "myvtab", | ||
| 1225 | &myvtab_module_context, | ||
| 1226 | TestVirtualTable, | ||
| 1227 | ); | ||
| 1228 | |||
| 1229 | var diags = Diagnostics{}; | ||
| 1230 | try db.exec("CREATE VIRTUAL TABLE foobar USING myvtab(content=hello, n=200)", .{ .diags = &diags }, .{}); | ||
| 1231 | |||
| 1232 | // Filter with both `foo` and `bar` | ||
| 1233 | |||
| 1234 | var stmt = try db.prepareWithDiags( | ||
| 1235 | "SELECT rowid, foo, bar, baz FROM foobar WHERE foo = ?{[]const u8} AND bar = ?{[]const u8} AND baz > ?{usize}", | ||
| 1236 | .{ .diags = &diags }, | ||
| 1237 | ); | ||
| 1238 | defer stmt.deinit(); | ||
| 1239 | |||
| 1240 | var rows_arena = heap.ArenaAllocator.init(testing.allocator); | ||
| 1241 | defer rows_arena.deinit(); | ||
| 1242 | |||
| 1243 | const rows = try stmt.all( | ||
| 1244 | struct { | ||
| 1245 | id: i64, | ||
| 1246 | foo: []const u8, | ||
| 1247 | bar: []const u8, | ||
| 1248 | baz: usize, | ||
| 1249 | }, | ||
| 1250 | rows_arena.allocator(), | ||
| 1251 | .{ .diags = &diags }, | ||
| 1252 | .{ | ||
| 1253 | .foo = @as([]const u8, "Vincent"), | ||
| 1254 | .bar = @as([]const u8, "Michel"), | ||
| 1255 | .baz = @as(usize, 2), | ||
| 1256 | }, | ||
| 1257 | ); | ||
| 1258 | try testing.expect(rows.len > 0); | ||
| 1259 | |||
| 1260 | for (rows) |row| { | ||
| 1261 | debug.print("result row: id={d} foo={s} bar={s} baz={d}\n", .{ row.id, row.foo, row.bar, row.baz }); | ||
| 1262 | } | ||
| 1263 | |||
| 1264 | // const row1 = rows[0]; | ||
| 1265 | // try testing.expectEqual(@as(i64, 0), row1.id); | ||
| 1266 | // try testing.expectEqualStrings("foo_hello", row1.foo); | ||
| 1267 | // try testing.expectEqualStrings("bar_hello", row1.bar); | ||
| 1268 | // try testing.expectEqual(@as(usize, 0), row1.baz); | ||
| 1269 | |||
| 1270 | // const row2 = rows[1]; | ||
| 1271 | // try testing.expectEqual(@as(i64, 1), row2.id); | ||
| 1272 | // try testing.expectEqualStrings("foo_hello", row2.foo); | ||
| 1273 | // try testing.expectEqualStrings("bar_hello", row2.bar); | ||
| 1274 | // try testing.expectEqual(@as(usize, 1), row2.baz); | ||
| 1275 | } | ||