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 { pub const TimelineResult = struct {
items: []services.notes.Note, items: []services.notes.NoteDetailed,
prev_page: TimelineArgs, prev_page: TimelineArgs,
next_page: TimelineArgs, next_page: TimelineArgs,

View file

@ -13,6 +13,13 @@ pub const CreateError = error{
DatabaseFailure, DatabaseFailure,
}; };
pub const ActorDetailed = struct {
id: Uuid,
username: []const u8,
host: []const u8,
created_at: DateTime,
};
pub const LookupError = error{ pub const LookupError = error{
DatabaseFailure, DatabaseFailure,
}; };

View file

@ -14,6 +14,17 @@ pub const Note = struct {
created_at: DateTime, 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{ pub const CreateError = error{
DatabaseFailure, DatabaseFailure,
}; };
@ -81,7 +92,7 @@ pub const QueryArgs = struct {
}; };
pub const QueryResult = struct { pub const QueryResult = struct {
items: []Note, items: []NoteDetailed,
prev_page: QueryArgs, prev_page: QueryArgs,
next_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); var builder = sql.QueryBuilder.init(alloc);
defer builder.deinit(); 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 \\ 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( const results = try db.queryRowsWithOptions(
Note, NoteDetailed,
try builder.terminate(), try builder.terminate(),
query_args, query_args,
max_items, 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. // Represents a set of results.
// row() must be called until it returns null, or the query may not complete // row() must be called until it returns null, or the query may not complete
// Must be deallocated by a call to finish() // Must be deallocated by a call to finish()
pub fn Results(comptime T: type) type { pub fn Results(comptime T: type) type {
// would normally make this a declaration of the struct, but it causes the compiler to crash // 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 { return struct {
const Self = @This(); const Self = @This();
@ -141,13 +189,16 @@ pub fn Results(comptime T: type) type {
return Self{ .underlying = underlying, .column_indices = blk: { return Self{ .underlying = underlying, .column_indices = blk: {
var indices: [fields.len]u15 = undefined; var indices: [fields.len]u15 = undefined;
inline for (fields) |f, i| { inline for (fields) |f, i| {
indices[i] = if (!std.meta.trait.isTuple(T)) if (comptime std.meta.trait.isTuple(T)) {
underlying.columnIndex(f.name) catch { indices[i] = i;
std.log.err("Could not find column index for field {s}", .{f.name}); } 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; return error.ColumnMismatch;
};
} }
else
i;
} }
break :blk indices; 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 // 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 // successfully allocate is defined at runtime. So we iterate over the entire field array and
// conditionally deallocate fields in the loop. // 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| { inline for (fields) |f, i| {
// TODO: Causes compiler segfault. why? // TODO: Causes compiler segfault. why?
//const F = f.field_type; //const F = f.field_type;
const F = @TypeOf(@field(result, f.name)); //const F = @TypeOf(@field(result, f.name));
@field(result, f.name) = row_val.get(F, self.column_indices[i], alloc) catch |err| { const F = std.meta.Child(FieldPtr(*@TypeOf(result), f));
std.log.err("SQL: Error getting column {s} of type {}", .{ f.name, 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; return err;
}; };
fields_allocated += 1; fields_allocated += 1;