summaryrefslogtreecommitdiff
path: root/vtab.zig
diff options
context:
space:
mode:
authorGravatar Vincent Rischmann2022-08-15 21:53:24 +0200
committerGravatar Vincent Rischmann2022-09-11 17:12:37 +0200
commit6ca5a280f4dc6d248f829cc76abe3149b646cea5 (patch)
tree1a4a6d2226e27648a21ed8df9a599bfd2da55ba3 /vtab.zig
parentmove test helpers (diff)
downloadzig-sqlite-6ca5a280f4dc6d248f829cc76abe3149b646cea5.tar.gz
zig-sqlite-6ca5a280f4dc6d248f829cc76abe3149b646cea5.tar.xz
zig-sqlite-6ca5a280f4dc6d248f829cc76abe3149b646cea5.zip
add the virtual table implementationvirtual-table-bak
Diffstat (limited to 'vtab.zig')
-rw-r--r--vtab.zig1275
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 @@
1const std = @import("std");
2const debug = std.debug;
3const fmt = std.fmt;
4const heap = std.heap;
5const mem = std.mem;
6const meta = std.meta;
7const testing = std.testing;
8
9const c = @import("c.zig").c;
10const versionGreaterThanOrEqualTo = @import("c.zig").versionGreaterThanOrEqualTo;
11const getTestDb = @import("test.zig").getTestDb;
12const Diagnostics = @import("sqlite.zig").Diagnostics;
13const Blob = @import("sqlite.zig").Blob;
14const Text = @import("sqlite.zig").Text;
15const helpers = @import("helpers.zig");
16
17const 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.
22pub const ModuleContext = struct {
23 allocator: mem.Allocator,
24};
25
26const 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.
29fn 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
41fn 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.
51pub 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
65pub 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.
290pub 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
302pub 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.
314fn 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.
474fn 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
545pub 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
943const 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
1102const 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
1215test "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}