summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--sqlite.zig25
-rw-r--r--vtab.zig1302
2 files changed, 1327 insertions, 0 deletions
diff --git a/sqlite.zig b/sqlite.zig
index 09e13ba..1fe8aea 100644
--- a/sqlite.zig
+++ b/sqlite.zig
@@ -20,8 +20,13 @@ const getLastDetailedErrorFromDb = errors.getLastDetailedErrorFromDb;
20const getDetailedErrorFromResultCode = errors.getDetailedErrorFromResultCode; 20const getDetailedErrorFromResultCode = errors.getDetailedErrorFromResultCode;
21 21
22const getTestDb = @import("test.zig").getTestDb; 22const getTestDb = @import("test.zig").getTestDb;
23pub const vtab = @import("vtab.zig");
23const helpers = @import("helpers.zig"); 24const helpers = @import("helpers.zig");
24 25
26test {
27 _ = @import("vtab.zig");
28}
29
25const logger = std.log.scoped(.sqlite); 30const logger = std.log.scoped(.sqlite);
26 31
27/// Text is used to represent a SQLite TEXT value when binding a parameter or reading a column. 32/// Text is used to represent a SQLite TEXT value when binding a parameter or reading a column.
@@ -787,6 +792,26 @@ pub const Db = struct {
787 try stmt.exec(new_options, .{}); 792 try stmt.exec(new_options, .{});
788 } 793 }
789 } 794 }
795
796 pub fn createVirtualTable(
797 self: *Self,
798 comptime name: [:0]const u8,
799 module_context: *vtab.ModuleContext,
800 comptime Table: type,
801 ) !void {
802 const VirtualTableType = vtab.VirtualTable(name, Table);
803
804 const result = c.sqlite3_create_module_v2(
805 self.db,
806 name,
807 &VirtualTableType.module,
808 module_context,
809 null,
810 );
811 if (result != c.SQLITE_OK) {
812 return errors.errorFromResultCode(result);
813 }
814 }
790}; 815};
791 816
792/// FunctionContext is the context passed as first parameter in the `step` and `finalize` functions used with `createAggregateFunction`. 817/// FunctionContext is the context passed as first parameter in the `step` and `finalize` functions used with `createAggregateFunction`.
diff --git a/vtab.zig b/vtab.zig
new file mode 100644
index 0000000..acaac42
--- /dev/null
+++ b/vtab.zig
@@ -0,0 +1,1302 @@
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
26fn 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.
36pub 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
50pub 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.
275pub 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
287pub 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.
299fn 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.
459fn 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
530pub const ModuleArgument = union(enum) {
531 kv: struct {
532 key: []const u8,
533 value: []const u8,
534 },
535 plain: []const u8,
536};
537
538const ParseModuleArgumentsError = error{} || mem.Allocator.Error;
539
540fn 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
563pub 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
960const 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
1117const 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
1226test "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
1280test "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}