summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Vincent Rischmann2020-11-12 15:10:56 +0100
committerGravatar Vincent Rischmann2020-11-12 15:10:56 +0100
commit55c1a3c35c35e19b5ca2b83f076e818f3ded9f94 (patch)
treec9444ab595c27cecc2bf8ee718437fc2055c0858
parentupdate requirements (diff)
parentallow untyped bind markers (diff)
downloadzig-sqlite-55c1a3c35c35e19b5ca2b83f076e818f3ded9f94.tar.gz
zig-sqlite-55c1a3c35c35e19b5ca2b83f076e818f3ded9f94.tar.xz
zig-sqlite-55c1a3c35c35e19b5ca2b83f076e818f3ded9f94.zip
Merge branch 'bind-parameters-types' into master
-rw-r--r--query.zig212
-rw-r--r--sqlite.zig164
2 files changed, 319 insertions, 57 deletions
diff --git a/query.zig b/query.zig
new file mode 100644
index 0000000..81f7333
--- /dev/null
+++ b/query.zig
@@ -0,0 +1,212 @@
1const builtin = @import("builtin");
2const std = @import("std");
3const mem = std.mem;
4const testing = std.testing;
5
6/// Blob is used to represent a SQLite BLOB value when binding a parameter or reading a column.
7pub const Blob = struct { data: []const u8 };
8
9/// Text is used to represent a SQLite TEXT value when binding a parameter or reading a column.
10pub const Text = struct { data: []const u8 };
11
12const BindMarker = union(enum) {
13 Typed: type,
14 Untyped: void,
15};
16
17pub const ParsedQuery = struct {
18 const Self = @This();
19
20 bind_markers: [128]BindMarker,
21 nb_bind_markers: usize,
22
23 query: [1024]u8,
24 query_size: usize,
25
26 pub fn from(comptime query: []const u8) Self {
27 const State = enum {
28 Start,
29 BindMarker,
30 BindMarkerType,
31 };
32
33 comptime var buf: [query.len]u8 = undefined;
34 comptime var pos = 0;
35 comptime var state = .Start;
36
37 comptime var current_bind_marker_type: [256]u8 = undefined;
38 comptime var current_bind_marker_type_pos = 0;
39
40 comptime var parsed_query: ParsedQuery = undefined;
41 parsed_query.nb_bind_markers = 0;
42
43 inline for (query) |c, i| {
44 switch (state) {
45 .Start => switch (c) {
46 '?' => {
47 state = .BindMarker;
48 buf[pos] = c;
49 pos += 1;
50 },
51 else => {
52 buf[pos] = c;
53 pos += 1;
54 },
55 },
56 .BindMarker => switch (c) {
57 '{' => {
58 state = .BindMarkerType;
59 current_bind_marker_type_pos = 0;
60 },
61 else => {
62 // This is a bind marker without a type.
63 state = .Start;
64
65 parsed_query.bind_markers[parsed_query.nb_bind_markers] = BindMarker{ .Untyped = {} };
66 parsed_query.nb_bind_markers += 1;
67
68 buf[pos] = c;
69 pos += 1;
70 },
71 },
72 .BindMarkerType => switch (c) {
73 '}' => {
74 state = .Start;
75
76 const typ = parsed_query.parseType(current_bind_marker_type[0..current_bind_marker_type_pos]);
77
78 parsed_query.bind_markers[parsed_query.nb_bind_markers] = BindMarker{ .Typed = typ };
79 parsed_query.nb_bind_markers += 1;
80 },
81 else => {
82 current_bind_marker_type[current_bind_marker_type_pos] = c;
83 current_bind_marker_type_pos += 1;
84 },
85 },
86 else => {
87 @compileError("invalid state " ++ @tagName(state));
88 },
89 }
90 }
91
92 // The last character was ? so this must be an untyped bind marker.
93 if (state == .BindMarker) {
94 parsed_query.bind_markers[parsed_query.nb_bind_markers] = BindMarker{ .Untyped = {} };
95 parsed_query.nb_bind_markers += 1;
96 }
97
98 if (state == .BindMarkerType) {
99 @compileError("invalid final state " ++ @tagName(state) ++ ", this means you wrote an incomplete bind marker type");
100 }
101
102 mem.copy(u8, &parsed_query.query, &buf);
103 parsed_query.query_size = pos;
104
105 return parsed_query;
106 }
107
108 fn parseType(comptime self: *Self, type_info: []const u8) type {
109 if (type_info.len <= 0) @compileError("invalid type info " ++ type_info);
110
111 // Integer
112 if (mem.eql(u8, "usize", type_info)) return usize;
113 if (mem.eql(u8, "isize", type_info)) return isize;
114
115 if (type_info[0] == 'u' or type_info[0] == 'i') {
116 return @Type(builtin.TypeInfo{
117 .Int = builtin.TypeInfo.Int{
118 .is_signed = type_info[0] == 'i',
119 .bits = std.fmt.parseInt(usize, type_info[1..type_info.len], 10) catch {
120 @compileError("invalid type info " ++ type_info);
121 },
122 },
123 });
124 }
125
126 // Float
127 if (mem.eql(u8, "f16", type_info)) return f16;
128 if (mem.eql(u8, "f32", type_info)) return f32;
129 if (mem.eql(u8, "f64", type_info)) return f64;
130 if (mem.eql(u8, "f128", type_info)) return f128;
131
132 // Strings
133 if (mem.eql(u8, "[]const u8", type_info) or mem.eql(u8, "[]u8", type_info)) {
134 return []const u8;
135 }
136 if (mem.eql(u8, "text", type_info)) return Text;
137 if (mem.eql(u8, "blob", type_info)) return Blob;
138
139 @compileError("invalid type info " ++ type_info);
140 }
141
142 pub fn getQuery(comptime self: *const Self) []const u8 {
143 return self.query[0..self.query_size];
144 }
145};
146
147test "parsed query: query" {
148 const testCase = struct {
149 query: []const u8,
150 expected_query: []const u8,
151 };
152
153 const testCases = &[_]testCase{
154 .{
155 .query = "INSERT INTO user(id, name, age) VALUES(?{usize}, ?{[]const u8}, ?{u32})",
156 .expected_query = "INSERT INTO user(id, name, age) VALUES(?, ?, ?)",
157 },
158 .{
159 .query = "SELECT id, name, age FROM user WHER age > ?{u32} AND age < ?{u32}",
160 .expected_query = "SELECT id, name, age FROM user WHER age > ? AND age < ?",
161 },
162 .{
163 .query = "SELECT id, name, age FROM user WHER age > ? AND age < ?",
164 .expected_query = "SELECT id, name, age FROM user WHER age > ? AND age < ?",
165 },
166 };
167
168 inline for (testCases) |tc| {
169 comptime var parsed_query = ParsedQuery.from(tc.query);
170 std.debug.print("parsed query: {}\n", .{parsed_query.getQuery()});
171 testing.expectEqualStrings(tc.expected_query, parsed_query.getQuery());
172 }
173}
174
175test "parsed query: bind markers types" {
176 const testCase = struct {
177 query: []const u8,
178 expected_marker: BindMarker,
179 };
180
181 const testCases = &[_]testCase{
182 .{
183 .query = "foobar ?{usize}",
184 .expected_marker = .{ .Typed = usize },
185 },
186 .{
187 .query = "foobar ?{text}",
188 .expected_marker = .{ .Typed = Text },
189 },
190 .{
191 .query = "foobar ?{blob}",
192 .expected_marker = .{ .Typed = Blob },
193 },
194 .{
195 .query = "foobar ?",
196 .expected_marker = .{ .Untyped = {} },
197 },
198 };
199
200 inline for (testCases) |tc| {
201 comptime var parsed_query = ParsedQuery.from(tc.query);
202 std.debug.print("parsed query: {}\n", .{parsed_query.getQuery()});
203
204 testing.expectEqual(1, parsed_query.nb_bind_markers);
205
206 const bind_marker = parsed_query.bind_markers[0];
207 switch (tc.expected_marker) {
208 .Typed => |typ| testing.expectEqual(typ, bind_marker.Typed),
209 .Untyped => |typ| testing.expectEqual(typ, bind_marker.Untyped),
210 }
211 }
212}
diff --git a/sqlite.zig b/sqlite.zig
index 5844c34..53e1a6a 100644
--- a/sqlite.zig
+++ b/sqlite.zig
@@ -8,6 +8,8 @@ const c = @cImport({
8 @cInclude("sqlite3.h"); 8 @cInclude("sqlite3.h");
9}); 9});
10 10
11usingnamespace @import("query.zig");
12
11const logger = std.log.scoped(.sqlite); 13const logger = std.log.scoped(.sqlite);
12 14
13/// Db is a wrapper around a SQLite database, providing high-level functions for executing queries. 15/// Db is a wrapper around a SQLite database, providing high-level functions for executing queries.
@@ -106,8 +108,10 @@ pub const Db = struct {
106 /// The statement returned is only compatible with the number of bind markers in the input query. 108 /// The statement returned is only compatible with the number of bind markers in the input query.
107 /// This is done because we type check the bind parameters when executing the statement later. 109 /// This is done because we type check the bind parameters when executing the statement later.
108 /// 110 ///
109 pub fn prepare(self: *Self, comptime query: []const u8) !Statement(StatementOptions.from(query)) { 111 pub fn prepare(self: *Self, comptime query: []const u8) !Statement(.{}, ParsedQuery.from(query)) {
110 return Statement(comptime StatementOptions.from(query)).prepare(self, 0, query); 112 @setEvalBranchQuota(3000);
113 const parsed_query = ParsedQuery.from(query);
114 return Statement(.{}, comptime parsed_query).prepare(self, 0);
111 } 115 }
112 116
113 /// rowsAffected returns the number of rows affected by the last statement executed. 117 /// rowsAffected returns the number of rows affected by the last statement executed.
@@ -116,28 +120,7 @@ pub const Db = struct {
116 } 120 }
117}; 121};
118 122
119/// Bytes is used to represent a byte slice with its SQLite datatype. 123pub const StatementOptions = struct {};
120///
121/// Since Zig doesn't have strings we can't tell if a []u8 must be stored as a SQLite TEXT or BLOB,
122/// this type can be used to communicate this when executing a statement.
123///
124/// If a []u8 or []const u8 is passed as bind parameter it will be treated as TEXT.
125pub const Bytes = union(enum) {
126 Blob: []const u8,
127 Text: []const u8,
128};
129
130pub const StatementOptions = struct {
131 const Self = @This();
132
133 bind_markers: usize,
134
135 fn from(comptime query: []const u8) Self {
136 return Self{
137 .bind_markers = std.mem.count(u8, query, "?"),
138 };
139 }
140};
141 124
142/// Statement is a wrapper around a SQLite statement, providing high-level functions to execute 125/// Statement is a wrapper around a SQLite statement, providing high-level functions to execute
143/// a statement and retrieve rows for SELECT queries. 126/// a statement and retrieve rows for SELECT queries.
@@ -172,19 +155,21 @@ pub const StatementOptions = struct {
172/// 155///
173/// Look at aach function for more complete documentation. 156/// Look at aach function for more complete documentation.
174/// 157///
175pub fn Statement(comptime opts: StatementOptions) type { 158pub fn Statement(comptime opts: StatementOptions, comptime query: ParsedQuery) type {
176 return struct { 159 return struct {
177 const Self = @This(); 160 const Self = @This();
178 161
179 stmt: *c.sqlite3_stmt, 162 stmt: *c.sqlite3_stmt,
180 163
181 fn prepare(db: *Db, flags: c_uint, comptime query: []const u8) !Self { 164 fn prepare(db: *Db, flags: c_uint) !Self {
182 var stmt = blk: { 165 var stmt = blk: {
166 const real_query = query.getQuery();
167
183 var tmp: ?*c.sqlite3_stmt = undefined; 168 var tmp: ?*c.sqlite3_stmt = undefined;
184 const result = c.sqlite3_prepare_v3( 169 const result = c.sqlite3_prepare_v3(
185 db.db, 170 db.db,
186 query.ptr, 171 real_query.ptr,
187 @intCast(c_int, query.len), 172 @intCast(c_int, real_query.len),
188 flags, 173 flags,
189 &tmp, 174 &tmp,
190 null, 175 null,
@@ -212,11 +197,19 @@ pub fn Statement(comptime opts: StatementOptions) type {
212 const StructType = @TypeOf(values); 197 const StructType = @TypeOf(values);
213 const StructTypeInfo = @typeInfo(StructType).Struct; 198 const StructTypeInfo = @typeInfo(StructType).Struct;
214 199
215 if (comptime opts.bind_markers != StructTypeInfo.fields.len) { 200 if (comptime query.nb_bind_markers != StructTypeInfo.fields.len) {
216 @compileError("number of bind markers not equal to number of fields"); 201 @compileError("number of bind markers not equal to number of fields");
217 } 202 }
218 203
219 inline for (StructTypeInfo.fields) |struct_field, _i| { 204 inline for (StructTypeInfo.fields) |struct_field, _i| {
205 const bind_marker = query.bind_markers[_i];
206 switch (bind_marker) {
207 .Typed => |typ| if (struct_field.field_type != typ) {
208 @compileError("value type " ++ @typeName(struct_field.field_type) ++ " is not the bind marker type " ++ @typeName(typ));
209 },
210 .Untyped => {},
211 }
212
220 const i = @as(usize, _i); 213 const i = @as(usize, _i);
221 const field_type_info = @typeInfo(struct_field.field_type); 214 const field_type_info = @typeInfo(struct_field.field_type);
222 const field_value = @field(values, struct_field.name); 215 const field_value = @field(values, struct_field.name);
@@ -226,10 +219,8 @@ pub fn Statement(comptime opts: StatementOptions) type {
226 []const u8, []u8 => { 219 []const u8, []u8 => {
227 _ = c.sqlite3_bind_text(self.stmt, column, field_value.ptr, @intCast(c_int, field_value.len), null); 220 _ = c.sqlite3_bind_text(self.stmt, column, field_value.ptr, @intCast(c_int, field_value.len), null);
228 }, 221 },
229 Bytes => switch (field_value) { 222 Text => _ = c.sqlite3_bind_text(self.stmt, column, field_value.data.ptr, @intCast(c_int, field_value.data.len), null),
230 .Text => |v| _ = c.sqlite3_bind_text(self.stmt, column, v.ptr, @intCast(c_int, v.len), null), 223 Blob => _ = c.sqlite3_bind_blob(self.stmt, column, field_value.data.ptr, @intCast(c_int, field_value.data.len), null),
231 .Blob => |v| _ = c.sqlite3_bind_blob(self.stmt, column, v.ptr, @intCast(c_int, v.len), null),
232 },
233 else => switch (field_type_info) { 224 else => switch (field_type_info) {
234 .Int, .ComptimeInt => _ = c.sqlite3_bind_int64(self.stmt, column, @intCast(c_longlong, field_value)), 225 .Int, .ComptimeInt => _ = c.sqlite3_bind_int64(self.stmt, column, @intCast(c_longlong, field_value)),
235 .Float, .ComptimeFloat => _ = c.sqlite3_bind_double(self.stmt, column, field_value), 226 .Float, .ComptimeFloat => _ = c.sqlite3_bind_double(self.stmt, column, field_value),
@@ -385,6 +376,39 @@ pub fn Statement(comptime opts: StatementOptions) type {
385 return @intCast(Type, n); 376 return @intCast(Type, n);
386 } 377 }
387 378
379 const ReadBytesMode = enum {
380 Blob,
381 Text,
382 };
383
384 fn readBytes(self: *Self, allocator: *mem.Allocator, mode: ReadBytesMode, _i: usize, ptr: *[]const u8) !void {
385 const i = @intCast(c_int, _i);
386 switch (mode) {
387 .Blob => {
388 const data = c.sqlite3_column_blob(self.stmt, i);
389 if (data == null) ptr.* = "";
390
391 const size = @intCast(usize, c.sqlite3_column_bytes(self.stmt, i));
392
393 var tmp = try allocator.alloc(u8, size);
394 mem.copy(u8, tmp, @ptrCast([*c]const u8, data)[0..size]);
395
396 ptr.* = tmp;
397 },
398 .Text => {
399 const data = c.sqlite3_column_text(self.stmt, i);
400 if (data == null) ptr.* = "";
401
402 const size = @intCast(usize, c.sqlite3_column_bytes(self.stmt, i));
403
404 var tmp = try allocator.alloc(u8, size);
405 mem.copy(u8, tmp, @ptrCast([*c]const u8, data)[0..size]);
406
407 ptr.* = tmp;
408 },
409 }
410 }
411
388 fn readStruct(self: *Self, comptime Type: type, options: anytype) !Type { 412 fn readStruct(self: *Self, comptime Type: type, options: anytype) !Type {
389 var value: Type = undefined; 413 var value: Type = undefined;
390 414
@@ -394,17 +418,13 @@ pub fn Statement(comptime opts: StatementOptions) type {
394 418
395 switch (field.field_type) { 419 switch (field.field_type) {
396 []const u8, []u8 => { 420 []const u8, []u8 => {
397 const data = c.sqlite3_column_blob(self.stmt, i); 421 try self.readBytes(options.allocator, .Blob, i, &@field(value, field.name));
398 if (data == null) { 422 },
399 @field(value, field.name) = ""; 423 Blob => {
400 } else { 424 try self.readBytes(options.allocator, .Blob, i, &@field(value, field.name).data);
401 const size = @intCast(usize, c.sqlite3_column_bytes(self.stmt, i)); 425 },
402 426 Text => {
403 var tmp = try options.allocator.alloc(u8, size); 427 try self.readBytes(options.allocator, .Text, i, &@field(value, field.name).data);
404 mem.copy(u8, tmp, @ptrCast([*c]const u8, data)[0..size]);
405
406 @field(value, field.name) = tmp;
407 }
408 }, 428 },
409 else => switch (field_type_info) { 429 else => switch (field_type_info) {
410 .Int => { 430 .Int => {
@@ -490,7 +510,7 @@ test "sqlite: statement exec" {
490 }; 510 };
491 511
492 for (users) |user| { 512 for (users) |user| {
493 try db.exec("INSERT INTO user(id, name, age) VALUES(?, ?, ?)", user); 513 try db.exec("INSERT INTO user(id, name, age) VALUES(?{usize}, ?{[]const u8}, ?{usize})", user);
494 514
495 const rows_inserted = db.rowsAffected(); 515 const rows_inserted = db.rowsAffected();
496 testing.expectEqual(@as(usize, 1), rows_inserted); 516 testing.expectEqual(@as(usize, 1), rows_inserted);
@@ -499,10 +519,10 @@ test "sqlite: statement exec" {
499 // Read a single user 519 // Read a single user
500 520
501 { 521 {
502 var stmt = try db.prepare("SELECT id, name, age FROM user WHERE id = ?"); 522 var stmt = try db.prepare("SELECT id, name, age FROM user WHERE id = ?{usize}");
503 defer stmt.deinit(); 523 defer stmt.deinit();
504 524
505 var rows = try stmt.all(User, .{ .allocator = allocator }, .{ .id = 20 }); 525 var rows = try stmt.all(User, .{ .allocator = allocator }, .{ .id = @as(usize, 20) });
506 for (rows) |row| { 526 for (rows) |row| {
507 testing.expectEqual(users[0].id, row.id); 527 testing.expectEqual(users[0].id, row.id);
508 testing.expectEqualStrings(users[0].name, row.name); 528 testing.expectEqualStrings(users[0].name, row.name);
@@ -529,7 +549,7 @@ test "sqlite: statement exec" {
529 // Test with anonymous structs 549 // Test with anonymous structs
530 550
531 { 551 {
532 var stmt = try db.prepare("SELECT id, name, age FROM user WHERE id = ?"); 552 var stmt = try db.prepare("SELECT id, name, age FROM user WHERE id = ?{usize}");
533 defer stmt.deinit(); 553 defer stmt.deinit();
534 554
535 var row = try stmt.one( 555 var row = try stmt.one(
@@ -539,7 +559,7 @@ test "sqlite: statement exec" {
539 age: usize, 559 age: usize,
540 }, 560 },
541 .{ .allocator = allocator }, 561 .{ .allocator = allocator },
542 .{ .id = 20 }, 562 .{ .id = @as(usize, 20) },
543 ); 563 );
544 testing.expect(row != null); 564 testing.expect(row != null);
545 565
@@ -552,26 +572,56 @@ test "sqlite: statement exec" {
552 // Test with a single integer 572 // Test with a single integer
553 573
554 { 574 {
555 const query = "SELECT age FROM user WHERE id = ?"; 575 const query = "SELECT age FROM user WHERE id = ?{usize}";
556 576
557 var stmt: Statement(StatementOptions.from(query)) = try db.prepare(query); 577 var stmt: Statement(.{}, ParsedQuery.from(query)) = try db.prepare(query);
558 defer stmt.deinit(); 578 defer stmt.deinit();
559 579
560 var age = try stmt.one(usize, .{}, .{ .id = 20 }); 580 var age = try stmt.one(usize, .{}, .{ .id = @as(usize, 20) });
561 testing.expect(age != null); 581 testing.expect(age != null);
562 582
563 testing.expectEqual(@as(usize, 33), age.?); 583 testing.expectEqual(@as(usize, 33), age.?);
564 } 584 }
565 585
566 // Test with a Bytes struct 586 // Test with a Blob struct
587 {
588 try db.exec("INSERT INTO user(id, name, age) VALUES(?{usize}, ?{blob}, ?{u32})", .{
589 .id = @as(usize, 200),
590 .name = Blob{ .data = "hello" },
591 .age = @as(u32, 20),
592 });
593 }
567 594
595 // Test with a Text struct
568 { 596 {
569 try db.exec("INSERT INTO user(id, name, age) VALUES(?, ?, ?)", .{ 597 try db.exec("INSERT INTO user(id, name, age) VALUES(?{usize}, ?{text}, ?{u32})", .{
570 .id = 200, 598 .id = @as(usize, 201),
571 .name = Bytes{ .Text = "hello" }, 599 .name = Text{ .data = "hello" },
572 .age = 20, 600 .age = @as(u32, 20),
573 }); 601 });
574 } 602 }
603
604 // Read in a Text struct
605 {
606 var stmt = try db.prepare("SELECT id, name, age FROM user WHERE id = ?{usize}");
607 defer stmt.deinit();
608
609 var row = try stmt.one(
610 struct {
611 id: usize,
612 name: Text,
613 age: usize,
614 },
615 .{ .allocator = allocator },
616 .{@as(usize, 20)},
617 );
618 testing.expect(row != null);
619
620 const exp = users[0];
621 testing.expectEqual(exp.id, row.?.id);
622 testing.expectEqualStrings(exp.name, row.?.name.data);
623 testing.expectEqual(exp.age, row.?.age);
624 }
575} 625}
576 626
577fn dbMode() Db.Mode { 627fn dbMode() Db.Mode {