rluv-zerver/src/.deps/zig-sqlite/query.zig
2023-07-21 17:51:33 -06:00

407 lines
13 KiB
Zig

const std = @import("std");
const mem = std.mem;
const testing = std.testing;
const Blob = @import("sqlite.zig").Blob;
const Text = @import("sqlite.zig").Text;
const BindMarker = struct {
/// Contains the expected type for a bind parameter which will be checked
/// at comptime when calling bind on a statement.
///
/// A null means the bind parameter is untyped so there won't be comptime checking.
typed: ?type = null,
};
fn isNamedIdentifierChar(c: u8) bool {
return std.ascii.isAlphabetic(c) or std.ascii.isDigit(c) or c == '_';
}
pub fn ParsedQuery(comptime query: []const u8) ParsedQueryState(query.len) {
// This contains the final SQL query after parsing with our
// own typed bind markers removed.
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;
// becomes part of our result
comptime var bind_markers: [128]BindMarker = undefined;
comptime var nb_bind_markers: usize = 0;
inline for (query) |c| {
switch (state) {
.start => switch (c) {
'?', ':', '@', '$' => {
bind_markers[nb_bind_markers] = BindMarker{};
current_bind_marker_type_pos = 0;
state = .bind_marker;
buf[pos] = c;
pos += 1;
},
'\'', '"' => {
state = .inside_string;
buf[pos] = c;
pos += 1;
},
else => {
buf[pos] = c;
pos += 1;
},
},
.inside_string => switch (c) {
'\'', '"' => {
state = .start;
buf[pos] = c;
pos += 1;
},
else => {
buf[pos] = c;
pos += 1;
},
},
.bind_marker => switch (c) {
'?', ':', '@', '$' => @compileError("invalid multiple '?', ':', '$' or '@'."),
'{' => {
state = .bind_marker_type;
},
else => {
if (isNamedIdentifierChar(c)) {
// This is the start of a named bind marker.
state = .bind_marker_identifier;
} else {
// This is a unnamed, untyped bind marker.
state = .start;
bind_markers[nb_bind_markers].typed = null;
nb_bind_markers += 1;
}
buf[pos] = c;
pos += 1;
},
},
.bind_marker_identifier => switch (c) {
'?', ':', '@', '$' => @compileError("unregconised multiple '?', ':', '$' or '@'."),
'{' => {
state = .bind_marker_type;
current_bind_marker_type_pos = 0;
},
else => {
if (!isNamedIdentifierChar(c)) {
// This marks the end of the named bind marker.
state = .start;
nb_bind_markers += 1;
}
buf[pos] = c;
pos += 1;
},
},
.bind_marker_type => switch (c) {
'}' => {
state = .start;
const type_info_string = current_bind_marker_type[0..current_bind_marker_type_pos];
// Handles optional types
const typ = if (type_info_string[0] == '?') blk: {
const child_type = ParseType(type_info_string[1..]);
break :blk @Type(std.builtin.Type{
.Optional = .{
.child = child_type,
},
});
} else blk: {
break :blk ParseType(type_info_string);
};
bind_markers[nb_bind_markers].typed = typ;
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));
},
}
}
// The last character was a bind marker prefix so this must be an untyped bind marker.
switch (state) {
.bind_marker => {
bind_markers[nb_bind_markers].typed = null;
nb_bind_markers += 1;
},
.bind_marker_identifier => {
nb_bind_markers += 1;
},
.start => {},
else => @compileError("invalid final state " ++ @tagName(state) ++ ", this means you wrote an incomplete bind marker type"),
}
var parsed_state = ParsedQueryState(query.len){
.bind_markers = bind_markers,
.nb_bind_markers = nb_bind_markers,
.query = undefined,
.query_len = pos,
};
std.mem.copy(u8, &parsed_state.query, &buf);
return parsed_state;
}
pub fn ParsedQueryState(comptime max_query_len: usize) type {
return struct {
const Self = @This();
bind_markers: [128]BindMarker,
nb_bind_markers: usize,
query: [max_query_len]u8,
query_len: usize,
pub fn getQuery(comptime self: *const Self) []const u8 {
return self.query[0..self.query_len];
}
};
}
fn ParseType(comptime 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(std.builtin.Type{
.Int = std.builtin.Type.Int{
.signedness = if (type_info[0] == 'i') .signed else .unsigned,
.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;
// Bool
if (mem.eql(u8, "bool", type_info)) return bool;
// 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);
}
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 < ?",
},
.{
.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| {
@setEvalBranchQuota(100000);
comptime var parsed_query = ParsedQuery(tc.query);
try testing.expectEqualStrings(tc.expected_query, parsed_query.getQuery());
}
}
test "parsed query: bind markers types" {
const testCase = struct {
query: []const u8,
expected_marker: BindMarker,
};
const prefixes = &[_][]const u8{
"?",
"?123",
":",
":hello",
"$",
"$foobar",
"@",
"@name",
};
inline for (prefixes) |prefix| {
const testCases = &[_]testCase{
.{
.query = "foobar " ++ prefix ++ "{usize}",
.expected_marker = .{ .typed = usize },
},
.{
.query = "foobar " ++ prefix ++ "{text}",
.expected_marker = .{ .typed = Text },
},
.{
.query = "foobar " ++ prefix ++ "{blob}",
.expected_marker = .{ .typed = Blob },
},
.{
.query = "foobar " ++ prefix,
.expected_marker = .{ .typed = null },
},
.{
.query = "foobar " ++ prefix ++ "{?[]const u8}",
.expected_marker = .{ .typed = ?[]const u8 },
},
};
inline for (testCases) |tc| {
@setEvalBranchQuota(100000);
comptime var parsed_query = ParsedQuery(tc.query);
try testing.expectEqual(1, parsed_query.nb_bind_markers);
const bind_marker = parsed_query.bind_markers[0];
try testing.expectEqual(tc.expected_marker.typed, bind_marker.typed);
}
}
}
test "parsed query: bind markers identifier" {
const testCase = struct {
query: []const u8,
expected_marker: BindMarker,
};
const testCases = &[_]testCase{
.{
.query = "foobar @ABC{usize}",
.expected_marker = .{ .typed = usize },
},
.{
.query = "foobar ?123{text}",
.expected_marker = .{ .typed = Text },
},
.{
.query = "foobar $abc{blob}",
.expected_marker = .{ .typed = Blob },
},
.{
.query = "foobar :430{u32}",
.expected_marker = .{ .typed = u32 },
},
.{
.query = "foobar ?123",
.expected_marker = .{},
},
.{
.query = "foobar :hola",
.expected_marker = .{},
},
.{
.query = "foobar @foo",
.expected_marker = .{},
},
};
inline for (testCases) |tc| {
comptime var parsed_query = ParsedQuery(tc.query);
try testing.expectEqual(@as(usize, 1), parsed_query.nb_bind_markers);
const bind_marker = parsed_query.bind_markers[0];
try testing.expectEqual(tc.expected_marker, bind_marker);
}
}
test "parsed query: query bind identifier" {
const testCase = struct {
query: []const u8,
expected_query: []const u8,
expected_nb_bind_markers: usize,
};
const testCases = &[_]testCase{
.{
.query = "INSERT INTO user(id, name, age) VALUES(@id{usize}, :name{[]const u8}, $age{u32})",
.expected_query = "INSERT INTO user(id, name, age) VALUES(@id, :name, $age)",
.expected_nb_bind_markers = 3,
},
.{
.query = "INSERT INTO user(id, name, age) VALUES($id, $name, $age)",
.expected_query = "INSERT INTO user(id, name, age) VALUES($id, $name, $age)",
.expected_nb_bind_markers = 3,
},
.{
.query = "SELECT id, name, age FROM user WHER age > :ageGT{u32} AND age < @ageLT{u32}",
.expected_query = "SELECT id, name, age FROM user WHER age > :ageGT AND age < @ageLT",
.expected_nb_bind_markers = 2,
},
.{
.query = "SELECT id, name, age FROM user WHER age > :ageGT AND age < $ageLT",
.expected_query = "SELECT id, name, age FROM user WHER age > :ageGT AND age < $ageLT",
.expected_nb_bind_markers = 2,
},
.{
.query = "SELECT id, name, age FROM user WHER age > $my_age{i32} AND age < :your_age{i32}",
.expected_query = "SELECT id, name, age FROM user WHER age > $my_age AND age < :your_age",
.expected_nb_bind_markers = 2,
},
};
inline for (testCases) |tc| {
@setEvalBranchQuota(100000);
comptime var parsed_query = ParsedQuery(tc.query);
try testing.expectEqualStrings(tc.expected_query, parsed_query.getQuery());
try testing.expectEqual(tc.expected_nb_bind_markers, parsed_query.nb_bind_markers);
}
}
test "parsed query: bind marker character inside string" {
const testCase = struct {
query: []const u8,
exp_bind_markers: comptime_int,
exp: []const u8,
};
const testCases = &[_]testCase{
.{
.query = "SELECT json_extract(metadata, '$.name') AS name FROM foobar",
.exp_bind_markers = 0,
.exp = "SELECT json_extract(metadata, '$.name') AS name FROM foobar",
},
.{
.query = "SELECT json_extract(metadata, '$.name') AS name FROM foobar WHERE name = $name{text}",
.exp_bind_markers = 1,
.exp = "SELECT json_extract(metadata, '$.name') AS name FROM foobar WHERE name = $name",
},
};
inline for (testCases) |tc| {
@setEvalBranchQuota(100000);
comptime var parsed_query = ParsedQuery(tc.query);
try testing.expectEqual(@as(usize, tc.exp_bind_markers), parsed_query.nb_bind_markers);
try testing.expectEqualStrings(tc.exp, parsed_query.getQuery());
}
}