diff --git a/src/api/lib.zig b/src/api/lib.zig index ab19097..4b2a434 100644 --- a/src/api/lib.zig +++ b/src/api/lib.zig @@ -85,7 +85,7 @@ pub const TimelineArgs = struct { }; pub const TimelineResult = struct { - items: []services.notes.Note, + items: []services.notes.NoteDetailed, prev_page: TimelineArgs, next_page: TimelineArgs, diff --git a/src/api/services/actors.zig b/src/api/services/actors.zig index 2a7f964..30dd6d2 100644 --- a/src/api/services/actors.zig +++ b/src/api/services/actors.zig @@ -13,6 +13,13 @@ pub const CreateError = error{ DatabaseFailure, }; +pub const ActorDetailed = struct { + id: Uuid, + username: []const u8, + host: []const u8, + created_at: DateTime, +}; + pub const LookupError = error{ DatabaseFailure, }; diff --git a/src/api/services/notes.zig b/src/api/services/notes.zig index 086a85a..f413f76 100644 --- a/src/api/services/notes.zig +++ b/src/api/services/notes.zig @@ -14,6 +14,17 @@ pub const Note = struct { created_at: DateTime, }; +pub const NoteDetailed = struct { + id: Uuid, + + author: struct { + id: Uuid, + username: []const u8, + }, + content: []const u8, + created_at: DateTime, +}; + pub const CreateError = error{ DatabaseFailure, }; @@ -81,7 +92,7 @@ pub const QueryArgs = struct { }; pub const QueryResult = struct { - items: []Note, + items: []NoteDetailed, prev_page: QueryArgs, next_page: QueryArgs, @@ -91,7 +102,9 @@ pub fn query(db: anytype, args: QueryArgs, alloc: std.mem.Allocator) !QueryResul var builder = sql.QueryBuilder.init(alloc); defer builder.deinit(); - try builder.appendSlice(selectStarFromNote ++ + try builder.appendSlice( + \\SELECT note.id, note.content, note.created_at, actor.id AS "author.id", actor.username AS "author.username" + \\FROM note \\ JOIN actor ON actor.id = note.author_id \\ ); @@ -140,7 +153,7 @@ pub fn query(db: anytype, args: QueryArgs, alloc: std.mem.Allocator) !QueryResul }; const results = try db.queryRowsWithOptions( - Note, + NoteDetailed, try builder.terminate(), query_args, max_items, diff --git a/src/sql/lib.zig b/src/sql/lib.zig index 9b1f599..88c2bf1 100644 --- a/src/sql/lib.zig +++ b/src/sql/lib.zig @@ -120,12 +120,60 @@ const RawResults = union(Engine) { } }; +fn FieldPtr(comptime Ptr: type, comptime names: []const []const u8) type { + if (names.len == 0) return Ptr; + + const T = std.meta.Child(Ptr); + + const field = for (@typeInfo(T).Struct.fields) |f| { + if (std.mem.eql(u8, f.name, names[0])) break f; + } else @compileError("Unknown field " ++ names[0] ++ " in type " ++ @typeName(T)); + + return FieldPtr(*field.field_type, names[1..]); +} + +fn fieldPtr(ptr: anytype, comptime names: []const []const u8) FieldPtr(@TypeOf(ptr), names) { + if (names.len == 0) return ptr; + + return fieldPtr(&@field(ptr.*, names[0]), names[1..]); +} + +fn isScalar(comptime T: type) bool { + if (comptime std.meta.trait.isZigString(T)) return true; + if (comptime std.meta.trait.isIntegral(T)) return true; + if (comptime std.meta.trait.isFloat(T)) return true; + if (comptime std.meta.trait.is(.Enum)(T)) return true; + if (T == bool) return true; + if (comptime std.meta.trait.hasFn("parse")(T)) return true; + + if (comptime std.meta.trait.is(.Optional)(T) and isScalar(std.meta.Child(T))) return true; + + return false; +} + +fn recursiveFieldPaths(comptime T: type, comptime prefix: []const []const u8) []const []const []const u8 { + comptime { + var fields: []const []const []const u8 = &.{}; + + for (std.meta.fields(T)) |f| { + const full_name = prefix ++ [_][]const u8{f.name}; + if (isScalar(f.field_type)) { + fields = fields ++ [_][]const []const u8{full_name}; + } else { + fields = fields ++ recursiveFieldPaths(f.field_type, full_name); + } + } + + return fields; + } +} + // Represents a set of results. // row() must be called until it returns null, or the query may not complete // Must be deallocated by a call to finish() pub fn Results(comptime T: type) type { // would normally make this a declaration of the struct, but it causes the compiler to crash - const fields = if (T == void) .{} else std.meta.fields(T); + const fields = if (T == void) .{} else recursiveFieldPaths(T, &.{}); return struct { const Self = @This(); @@ -141,13 +189,16 @@ pub fn Results(comptime T: type) type { return Self{ .underlying = underlying, .column_indices = blk: { var indices: [fields.len]u15 = undefined; inline for (fields) |f, i| { - indices[i] = if (!std.meta.trait.isTuple(T)) - underlying.columnIndex(f.name) catch { - std.log.err("Could not find column index for field {s}", .{f.name}); + if (comptime std.meta.trait.isTuple(T)) { + indices[i] = i; + } else { + const name = util.comptimeJoin(".", f); + indices[i] = + underlying.columnIndex(name) catch { + std.log.err("Could not find column index for field {s}", .{name}); return error.ColumnMismatch; - } - else - i; + }; + } } break :blk indices; } }; @@ -168,15 +219,19 @@ pub fn Results(comptime T: type) type { // Iteration bounds must be defined at comptime (inline for) but the number of fields we could // successfully allocate is defined at runtime. So we iterate over the entire field array and // conditionally deallocate fields in the loop. - if (i < fields_allocated) util.deepFree(alloc, @field(result, f.name)); + const ptr = fieldPtr(&result, f); + if (i < fields_allocated) util.deepFree(alloc, ptr.*); }; inline for (fields) |f, i| { // TODO: Causes compiler segfault. why? //const F = f.field_type; - const F = @TypeOf(@field(result, f.name)); - @field(result, f.name) = row_val.get(F, self.column_indices[i], alloc) catch |err| { - std.log.err("SQL: Error getting column {s} of type {}", .{ f.name, F }); + //const F = @TypeOf(@field(result, f.name)); + const F = std.meta.Child(FieldPtr(*@TypeOf(result), f)); + const ptr = fieldPtr(&result, f); + const name = util.comptimeJoin(".", f); + ptr.* = row_val.get(F, self.column_indices[i], alloc) catch |err| { + std.log.err("SQL: Error getting column {s} of type {}", .{ name, F }); return err; }; fields_allocated += 1;