diff --git a/src/api/services/common.zig b/src/api/services/common.zig new file mode 100644 index 0000000..8a407d5 --- /dev/null +++ b/src/api/services/common.zig @@ -0,0 +1,16 @@ +const std = @import("std"); +const util = @import("util"); + +pub const Direction = enum { + ascending, + descending, + + pub const jsonStringify = util.jsonSerializeEnumAsString; +}; + +pub const PageDirection = enum { + forward, + backward, + + pub const jsonStringify = util.jsonSerializeEnumAsString; +}; diff --git a/src/api/services/communities.zig b/src/api/services/communities.zig index 84f07b6..8f0e21d 100644 --- a/src/api/services/communities.zig +++ b/src/api/services/communities.zig @@ -2,6 +2,7 @@ const std = @import("std"); const builtin = @import("builtin"); const util = @import("util"); const sql = @import("sql"); +const common = @import("./common.zig"); const Uuid = util.Uuid; const DateTime = util.DateTime; @@ -153,20 +154,8 @@ pub const QueryArgs = struct { pub const jsonStringify = util.jsonSerializeEnumAsString; }; - pub const Direction = enum { - ascending, - descending, - - pub const jsonStringify = util.jsonSerializeEnumAsString; - }; - - pub const PageDirection = enum { - forward, - backward, - - pub const jsonStringify = util.jsonSerializeEnumAsString; - }; - + pub const Direction = common.Direction; + pub const PageDirection = common.PageDirection; pub const Prev = std.meta.Child(std.meta.fieldInfo(QueryArgs, .prev).field_type); pub const OrderVal = std.meta.fieldInfo(Prev, .order_val).field_type; @@ -211,30 +200,6 @@ pub const QueryResult = struct { next_page: QueryArgs, }; -const QueryBuilder = struct { - array: std.ArrayList(u8), - where_clauses_appended: usize = 0, - - pub fn init(alloc: std.mem.Allocator) QueryBuilder { - return QueryBuilder{ .array = std.ArrayList(u8).init(alloc) }; - } - - pub fn deinit(self: *const QueryBuilder) void { - self.array.deinit(); - } - - pub fn andWhere(self: *QueryBuilder, clause: []const u8) !void { - if (self.where_clauses_appended == 0) { - try self.array.appendSlice("WHERE "); - } else { - try self.array.appendSlice(" AND "); - } - - try self.array.appendSlice(clause); - self.where_clauses_appended += 1; - } -}; - const max_max_items = 100; pub const QueryError = error{ @@ -246,7 +211,7 @@ pub const QueryError = error{ // arguments. // `args.max_items` is only a request, and fewer entries may be returned. pub fn query(db: anytype, args: QueryArgs, alloc: std.mem.Allocator) !QueryResult { - var builder = QueryBuilder.init(alloc); + var builder = sql.QueryBuilder.init(alloc); defer builder.deinit(); try builder.array.appendSlice( @@ -266,21 +231,21 @@ pub fn query(db: anytype, args: QueryArgs, alloc: std.mem.Allocator) !QueryResul if (args.prev) |prev| { if (prev.order_val != args.order_by) return error.PageArgMismatch; - try builder.andWhere(switch (args.order_by) { - .name => "(name, id)", - .host => "(host, id)", - .created_at => "(created_at, id)", - }); - _ = try builder.array.appendSlice(switch (args.direction) { + switch (args.order_by) { + .name => try builder.andWhere("(name, id)"), + .host => try builder.andWhere("(host, id)"), + .created_at => try builder.andWhere("(created_at, id)"), + } + switch (args.direction) { .ascending => switch (args.page_direction) { - .forward => " > ", - .backward => " < ", + .forward => try builder.andWhere(" > "), + .backward => try builder.andWhere(" < "), }, .descending => switch (args.page_direction) { - .forward => " < ", - .backward => " > ", + .forward => try builder.andWhere(" < "), + .backward => try builder.andWhere(" > "), }, - }); + } _ = try builder.array.appendSlice("($5, $6)"); } diff --git a/src/sql/lib.zig b/src/sql/lib.zig index 3124863..fc673ad 100644 --- a/src/sql/lib.zig +++ b/src/sql/lib.zig @@ -25,6 +25,47 @@ pub const Engine = enum { sqlite, }; +/// Helper for building queries at runtime. All constituent parts of the +/// query should be defined at comptime, however the choice of whether +/// or not to include them can occur at runtime. +pub const QueryBuilder = struct { + array: std.ArrayList(u8), + where_clauses_appended: usize = 0, + + pub fn init(alloc: std.mem.Allocator) QueryBuilder { + return QueryBuilder{ .array = std.ArrayList(u8).init(alloc) }; + } + + pub fn deinit(self: *const QueryBuilder) void { + self.array.deinit(); + } + + /// Add a chunk of sql to the query without processing + pub fn appendSlice(self: *QueryBuilder, comptime sql: []const u8) !void { + try self.array.appendSlice(sql); + } + + /// Add a where clause to the query. Clauses are assumed to be components + /// in an overall expression in Conjunctive Normal Form (AND of OR's). + /// https://en.wikipedia.org/wiki/Conjunctive_normal_form + /// All calls to andWhere must be contiguous, that is, they cannot be + /// interspersed with calls to appendSlice + pub fn andWhere(self: *QueryBuilder, comptime clause: []const u8) !void { + if (self.where_clauses_appended == 0) { + try self.array.appendSlice("WHERE "); + } else { + try self.array.appendSlice(" AND "); + } + + try self.array.appendSlice(clause); + self.where_clauses_appended += 1; + } + + pub fn str(self: *const QueryBuilder) []const u8 { + return self.array.items; + } +}; + // TODO: make this suck less pub const Config = union(Engine) { postgres: struct {