diff options
Diffstat (limited to '')
| -rw-r--r-- | query.zig | 212 |
1 files changed, 212 insertions, 0 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 @@ | |||
| 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 | Typed: type, | ||
| 14 | Untyped: 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 | // 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 | |||
| 147 | test "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 | |||
| 175 | test "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 | } | ||