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