From be6902d0736178c113d6b11c9b056c4a33a966f3 Mon Sep 17 00:00:00 2001 From: Vincent Rischmann Date: Fri, 30 Oct 2020 13:49:05 +0100 Subject: add types to bind markers and check them at comptime --- query.zig | 190 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 query.zig (limited to 'query.zig') diff --git a/query.zig b/query.zig new file mode 100644 index 0000000..a2a5c5c --- /dev/null +++ b/query.zig @@ -0,0 +1,190 @@ +const builtin = @import("builtin"); +const std = @import("std"); +const mem = std.mem; +const testing = std.testing; + +/// Blob is used to represent a SQLite BLOB value when binding a parameter or reading a column. +pub const Blob = struct { data: []const u8 }; + +/// Text is used to represent a SQLite TEXT value when binding a parameter or reading a column. +pub const Text = struct { data: []const u8 }; + +const BindMarker = union(enum) { + Type: type, + None: void, +}; + +pub const ParsedQuery = struct { + const Self = @This(); + + bind_markers: [128]BindMarker, + nb_bind_markers: usize, + + query: [1024]u8, + query_size: usize, + + pub fn from(comptime query: []const u8) Self { + const State = enum { + Start, + BindMarker, + BindMarkerType, + }; + + comptime var buf: [query.len]u8 = undefined; + comptime var pos = 0; + comptime var state = .Start; + + comptime var current_bind_marker_type: [256]u8 = undefined; + comptime var current_bind_marker_type_pos = 0; + + comptime var parsed_query: ParsedQuery = undefined; + parsed_query.nb_bind_markers = 0; + + inline for (query) |c, i| { + switch (state) { + .Start => switch (c) { + '?' => { + state = .BindMarker; + buf[pos] = c; + pos += 1; + }, + else => { + buf[pos] = c; + pos += 1; + }, + }, + .BindMarker => switch (c) { + '{' => { + state = .BindMarkerType; + current_bind_marker_type_pos = 0; + }, + else => { + @compileError("a bind marker start (the character ?) must be followed by a bind marker type, eg {integer}"); + }, + }, + .BindMarkerType => switch (c) { + '}' => { + state = .Start; + + const typ = parsed_query.parseType(current_bind_marker_type[0..current_bind_marker_type_pos]); + + parsed_query.bind_markers[parsed_query.nb_bind_markers] = BindMarker{ .Type = typ }; + parsed_query.nb_bind_markers += 1; + }, + else => { + current_bind_marker_type[current_bind_marker_type_pos] = c; + current_bind_marker_type_pos += 1; + }, + }, + else => { + @compileError("invalid state " ++ @tagName(state)); + }, + } + } + if (state == .BindMarker) { + @compileError("invalid final state " ++ @tagName(state) ++ ", this means you wrote a ? in last position without a bind marker type"); + } + if (state == .BindMarkerType) { + @compileError("invalid final state " ++ @tagName(state) ++ ", this means you wrote an incomplete bind marker type"); + } + + mem.copy(u8, &parsed_query.query, &buf); + parsed_query.query_size = pos; + + return parsed_query; + } + + fn parseType(comptime self: *Self, type_info: []const u8) type { + if (type_info.len <= 0) @compileError("invalid type info " ++ type_info); + + // Integer + if (mem.eql(u8, "usize", type_info)) return usize; + if (mem.eql(u8, "isize", type_info)) return isize; + + if (type_info[0] == 'u' or type_info[0] == 'i') { + return @Type(builtin.TypeInfo{ + .Int = builtin.TypeInfo.Int{ + .is_signed = type_info[0] == 'i', + .bits = std.fmt.parseInt(usize, type_info[1..type_info.len], 10) catch { + @compileError("invalid type info " ++ type_info); + }, + }, + }); + } + + // Float + if (mem.eql(u8, "f16", type_info)) return f16; + if (mem.eql(u8, "f32", type_info)) return f32; + if (mem.eql(u8, "f64", type_info)) return f64; + if (mem.eql(u8, "f128", type_info)) return f128; + + // Strings + if (mem.eql(u8, "[]const u8", type_info) or mem.eql(u8, "[]u8", type_info)) { + return []const u8; + } + if (mem.eql(u8, "text", type_info)) return Text; + if (mem.eql(u8, "blob", type_info)) return Blob; + + @compileError("invalid type info " ++ type_info); + } + + pub fn getQuery(comptime self: *const Self) []const u8 { + return self.query[0..self.query_size]; + } +}; + +test "parsed query: query" { + const testCase = struct { + query: []const u8, + expected_query: []const u8, + }; + + const testCases = &[_]testCase{ + .{ + .query = "INSERT INTO user(id, name, age) VALUES(?{usize}, ?{[]const u8}, ?{u32})", + .expected_query = "INSERT INTO user(id, name, age) VALUES(?, ?, ?)", + }, + .{ + .query = "SELECT id, name, age FROM user WHER age > ?{u32} AND age < ?{u32}", + .expected_query = "SELECT id, name, age FROM user WHER age > ? AND age < ?", + }, + }; + + inline for (testCases) |tc| { + comptime var parsed_query = ParsedQuery.from(tc.query); + std.debug.print("parsed query: {}\n", .{parsed_query.getQuery()}); + testing.expectEqualStrings(tc.expected_query, parsed_query.getQuery()); + } +} + +test "parsed query: bind markers types" { + const testCase = struct { + query: []const u8, + expected_marker: BindMarker, + }; + + const testCases = &[_]testCase{ + .{ + .query = "foobar ?{usize}", + .expected_marker = .{ .Type = usize }, + }, + .{ + .query = "foobar ?{text}", + .expected_marker = .{ .Type = Text }, + }, + .{ + .query = "foobar ?{blob}", + .expected_marker = .{ .Type = Blob }, + }, + }; + + inline for (testCases) |tc| { + comptime var parsed_query = ParsedQuery.from(tc.query); + std.debug.print("parsed query: {}\n", .{parsed_query.getQuery()}); + + testing.expectEqual(1, parsed_query.nb_bind_markers); + + const bind_marker = parsed_query.bind_markers[0]; + testing.expectEqual(tc.expected_marker.Type, bind_marker.Type); + } +} -- cgit v1.2.3 From 0d766361474ae47a2f21464913e348702705cf4e Mon Sep 17 00:00:00 2001 From: Vincent Rischmann Date: Wed, 11 Nov 2020 14:13:31 +0100 Subject: allow untyped bind markers --- query.zig | 40 +++++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) (limited to 'query.zig') diff --git a/query.zig b/query.zig index a2a5c5c..81f7333 100644 --- a/query.zig +++ b/query.zig @@ -10,8 +10,8 @@ pub const Blob = struct { data: []const u8 }; pub const Text = struct { data: []const u8 }; const BindMarker = union(enum) { - Type: type, - None: void, + Typed: type, + Untyped: void, }; pub const ParsedQuery = struct { @@ -59,7 +59,14 @@ pub const ParsedQuery = struct { current_bind_marker_type_pos = 0; }, else => { - @compileError("a bind marker start (the character ?) must be followed by a bind marker type, eg {integer}"); + // This is a bind marker without a type. + state = .Start; + + parsed_query.bind_markers[parsed_query.nb_bind_markers] = BindMarker{ .Untyped = {} }; + parsed_query.nb_bind_markers += 1; + + buf[pos] = c; + pos += 1; }, }, .BindMarkerType => switch (c) { @@ -68,7 +75,7 @@ pub const ParsedQuery = struct { const typ = parsed_query.parseType(current_bind_marker_type[0..current_bind_marker_type_pos]); - parsed_query.bind_markers[parsed_query.nb_bind_markers] = BindMarker{ .Type = typ }; + parsed_query.bind_markers[parsed_query.nb_bind_markers] = BindMarker{ .Typed = typ }; parsed_query.nb_bind_markers += 1; }, else => { @@ -81,9 +88,13 @@ pub const ParsedQuery = struct { }, } } + + // The last character was ? so this must be an untyped bind marker. if (state == .BindMarker) { - @compileError("invalid final state " ++ @tagName(state) ++ ", this means you wrote a ? in last position without a bind marker type"); + parsed_query.bind_markers[parsed_query.nb_bind_markers] = BindMarker{ .Untyped = {} }; + parsed_query.nb_bind_markers += 1; } + if (state == .BindMarkerType) { @compileError("invalid final state " ++ @tagName(state) ++ ", this means you wrote an incomplete bind marker type"); } @@ -148,6 +159,10 @@ test "parsed query: query" { .query = "SELECT id, name, age FROM user WHER age > ?{u32} AND age < ?{u32}", .expected_query = "SELECT id, name, age FROM user WHER age > ? AND age < ?", }, + .{ + .query = "SELECT id, name, age FROM user WHER age > ? AND age < ?", + .expected_query = "SELECT id, name, age FROM user WHER age > ? AND age < ?", + }, }; inline for (testCases) |tc| { @@ -166,15 +181,19 @@ test "parsed query: bind markers types" { const testCases = &[_]testCase{ .{ .query = "foobar ?{usize}", - .expected_marker = .{ .Type = usize }, + .expected_marker = .{ .Typed = usize }, }, .{ .query = "foobar ?{text}", - .expected_marker = .{ .Type = Text }, + .expected_marker = .{ .Typed = Text }, }, .{ .query = "foobar ?{blob}", - .expected_marker = .{ .Type = Blob }, + .expected_marker = .{ .Typed = Blob }, + }, + .{ + .query = "foobar ?", + .expected_marker = .{ .Untyped = {} }, }, }; @@ -185,6 +204,9 @@ test "parsed query: bind markers types" { testing.expectEqual(1, parsed_query.nb_bind_markers); const bind_marker = parsed_query.bind_markers[0]; - testing.expectEqual(tc.expected_marker.Type, bind_marker.Type); + switch (tc.expected_marker) { + .Typed => |typ| testing.expectEqual(typ, bind_marker.Typed), + .Untyped => |typ| testing.expectEqual(typ, bind_marker.Untyped), + } } } -- cgit v1.2.3