diff options
Diffstat (limited to 'query.zig')
| -rw-r--r-- | query.zig | 190 |
1 files changed, 190 insertions, 0 deletions
diff --git a/query.zig b/query.zig new file mode 100644 index 0000000..a2a5c5c --- /dev/null +++ b/query.zig | |||
| @@ -0,0 +1,190 @@ | |||
| 1 | const builtin = @import("builtin"); | ||
| 2 | const std = @import("std"); | ||
| 3 | const mem = std.mem; | ||
| 4 | const testing = std.testing; | ||
| 5 | |||
| 6 | /// Blob is used to represent a SQLite BLOB value when binding a parameter or reading a column. | ||
| 7 | pub 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. | ||
| 10 | pub const Text = struct { data: []const u8 }; | ||
| 11 | |||
| 12 | const BindMarker = union(enum) { | ||
| 13 | Type: type, | ||
| 14 | None: void, | ||
| 15 | }; | ||
| 16 | |||
| 17 | pub 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 | @compileError("a bind marker start (the character ?) must be followed by a bind marker type, eg {integer}"); | ||
| 63 | }, | ||
| 64 | }, | ||
| 65 | .BindMarkerType => switch (c) { | ||
| 66 | '}' => { | ||
| 67 | state = .Start; | ||
| 68 | |||
| 69 | const typ = parsed_query.parseType(current_bind_marker_type[0..current_bind_marker_type_pos]); | ||
| 70 | |||
| 71 | parsed_query.bind_markers[parsed_query.nb_bind_markers] = BindMarker{ .Type = typ }; | ||
| 72 | parsed_query.nb_bind_markers += 1; | ||
| 73 | }, | ||
| 74 | else => { | ||
| 75 | current_bind_marker_type[current_bind_marker_type_pos] = c; | ||
| 76 | current_bind_marker_type_pos += 1; | ||
| 77 | }, | ||
| 78 | }, | ||
| 79 | else => { | ||
| 80 | @compileError("invalid state " ++ @tagName(state)); | ||
| 81 | }, | ||
| 82 | } | ||
| 83 | } | ||
| 84 | if (state == .BindMarker) { | ||
| 85 | @compileError("invalid final state " ++ @tagName(state) ++ ", this means you wrote a ? in last position without a bind marker type"); | ||
| 86 | } | ||
| 87 | if (state == .BindMarkerType) { | ||
| 88 | @compileError("invalid final state " ++ @tagName(state) ++ ", this means you wrote an incomplete bind marker type"); | ||
| 89 | } | ||
| 90 | |||
| 91 | mem.copy(u8, &parsed_query.query, &buf); | ||
| 92 | parsed_query.query_size = pos; | ||
| 93 | |||
| 94 | return parsed_query; | ||
| 95 | } | ||
| 96 | |||
| 97 | fn parseType(comptime self: *Self, type_info: []const u8) type { | ||
| 98 | if (type_info.len <= 0) @compileError("invalid type info " ++ type_info); | ||
| 99 | |||
| 100 | // Integer | ||
| 101 | if (mem.eql(u8, "usize", type_info)) return usize; | ||
| 102 | if (mem.eql(u8, "isize", type_info)) return isize; | ||
| 103 | |||
| 104 | if (type_info[0] == 'u' or type_info[0] == 'i') { | ||
| 105 | return @Type(builtin.TypeInfo{ | ||
| 106 | .Int = builtin.TypeInfo.Int{ | ||
| 107 | .is_signed = type_info[0] == 'i', | ||
| 108 | .bits = std.fmt.parseInt(usize, type_info[1..type_info.len], 10) catch { | ||
| 109 | @compileError("invalid type info " ++ type_info); | ||
| 110 | }, | ||
| 111 | }, | ||
| 112 | }); | ||
| 113 | } | ||
| 114 | |||
| 115 | // Float | ||
| 116 | if (mem.eql(u8, "f16", type_info)) return f16; | ||
| 117 | if (mem.eql(u8, "f32", type_info)) return f32; | ||
| 118 | if (mem.eql(u8, "f64", type_info)) return f64; | ||
| 119 | if (mem.eql(u8, "f128", type_info)) return f128; | ||
| 120 | |||
| 121 | // Strings | ||
| 122 | if (mem.eql(u8, "[]const u8", type_info) or mem.eql(u8, "[]u8", type_info)) { | ||
| 123 | return []const u8; | ||
| 124 | } | ||
| 125 | if (mem.eql(u8, "text", type_info)) return Text; | ||
| 126 | if (mem.eql(u8, "blob", type_info)) return Blob; | ||
| 127 | |||
| 128 | @compileError("invalid type info " ++ type_info); | ||
| 129 | } | ||
| 130 | |||
| 131 | pub fn getQuery(comptime self: *const Self) []const u8 { | ||
| 132 | return self.query[0..self.query_size]; | ||
| 133 | } | ||
| 134 | }; | ||
| 135 | |||
| 136 | test "parsed query: query" { | ||
| 137 | const testCase = struct { | ||
| 138 | query: []const u8, | ||
| 139 | expected_query: []const u8, | ||
| 140 | }; | ||
| 141 | |||
| 142 | const testCases = &[_]testCase{ | ||
| 143 | .{ | ||
| 144 | .query = "INSERT INTO user(id, name, age) VALUES(?{usize}, ?{[]const u8}, ?{u32})", | ||
| 145 | .expected_query = "INSERT INTO user(id, name, age) VALUES(?, ?, ?)", | ||
| 146 | }, | ||
| 147 | .{ | ||
| 148 | .query = "SELECT id, name, age FROM user WHER age > ?{u32} AND age < ?{u32}", | ||
| 149 | .expected_query = "SELECT id, name, age FROM user WHER age > ? AND age < ?", | ||
| 150 | }, | ||
| 151 | }; | ||
| 152 | |||
| 153 | inline for (testCases) |tc| { | ||
| 154 | comptime var parsed_query = ParsedQuery.from(tc.query); | ||
| 155 | std.debug.print("parsed query: {}\n", .{parsed_query.getQuery()}); | ||
| 156 | testing.expectEqualStrings(tc.expected_query, parsed_query.getQuery()); | ||
| 157 | } | ||
| 158 | } | ||
| 159 | |||
| 160 | test "parsed query: bind markers types" { | ||
| 161 | const testCase = struct { | ||
| 162 | query: []const u8, | ||
| 163 | expected_marker: BindMarker, | ||
| 164 | }; | ||
| 165 | |||
| 166 | const testCases = &[_]testCase{ | ||
| 167 | .{ | ||
| 168 | .query = "foobar ?{usize}", | ||
| 169 | .expected_marker = .{ .Type = usize }, | ||
| 170 | }, | ||
| 171 | .{ | ||
| 172 | .query = "foobar ?{text}", | ||
| 173 | .expected_marker = .{ .Type = Text }, | ||
| 174 | }, | ||
| 175 | .{ | ||
| 176 | .query = "foobar ?{blob}", | ||
| 177 | .expected_marker = .{ .Type = Blob }, | ||
| 178 | }, | ||
| 179 | }; | ||
| 180 | |||
| 181 | inline for (testCases) |tc| { | ||
| 182 | comptime var parsed_query = ParsedQuery.from(tc.query); | ||
| 183 | std.debug.print("parsed query: {}\n", .{parsed_query.getQuery()}); | ||
| 184 | |||
| 185 | testing.expectEqual(1, parsed_query.nb_bind_markers); | ||
| 186 | |||
| 187 | const bind_marker = parsed_query.bind_markers[0]; | ||
| 188 | testing.expectEqual(tc.expected_marker.Type, bind_marker.Type); | ||
| 189 | } | ||
| 190 | } | ||