summaryrefslogtreecommitdiff
path: root/query.zig
diff options
context:
space:
mode:
Diffstat (limited to 'query.zig')
-rw-r--r--query.zig212
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 @@
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}