Support for nested structs in sql layer

This commit is contained in:
jaina heartles 2022-11-16 02:23:14 -08:00
parent e4a04b869e
commit 305c5f8c92
4 changed files with 90 additions and 15 deletions

View file

@ -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,

View file

@ -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,
};

View file

@ -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,

View file

@ -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;