Comptime Zig ORM

24 Likes

Love the “enum as distinct integer type” trick, that’s brilliant.

1 Like

Awesome! I was hoping for your blog post extrapolating this idea.

1 Like

This pattern is all over the Zig compiler and standard library, too! It’s referred to as “enum index” in Zig Patterns.

6 Likes

I absolutely love this! I’ve been looking in this direction too and it is just so cool.

I’m super-impressed with how naturally that filtering API works!

1 Like

Great Article, I’m going to try out your call to action and implement the db api myself.

3 Likes

Finally got around to doing this (see my attached code below). I opted to not implement the index portion of the challenge, so my solution is pretty small. Still it was the most I’ve ever done with comptime reflection, and was really fun. It’s a pretty powerful mechanism, though i can only think of a few use cases where i think I would reach for it. Thanks for the write up and challenge!

Solution
const std = @import("std");
const Type = std.builtin.Type;

pub fn DBType(comptime config: anytype) type{
    // Comptime Generation of the tables
    const table_defs = @typeInfo(@FieldType(@TypeOf(config), "tables")).@"struct".fields;

    var fields: [table_defs.len]Type.StructField = undefined;

    for (table_defs, &fields) |old, *field| {
        const T = old.defaultValue().?;
        const tbl = Table(T);
        const new: Type.StructField = .{
            .name = old.name,
            .alignment = std.meta.alignment(tbl),
            .default_value_ptr = &tbl{},
            .is_comptime = false,
            .type = tbl,
        };
        field.* = new;
    }
    const new_type: Type = .{ .@"struct" = .{ 
        .layout = .auto, 
        .decls = &[0]Type.Declaration{},
        .fields = &fields,
        .is_tuple = false,
        },
    };

    return @Type(new_type);
}

pub fn Table(T: type) type {

    return struct {
        // making an In memory DB to deal with this;
        data: std.ArrayListUnmanaged(T) = .empty,
        const Self = @This();
        pub fn create (self: *Self, gpa: std.mem.Allocator, data: T) !T.ID {
            var input = data;
            std.debug.print("Make Insert Statement", .{});
            // This does the insertion logic in our "in memory db"
            input.id = @enumFromInt(self.data.items.len + 1);
            try self.data.append(gpa, input);
            return input.id;
        }

        pub fn update (self: Self, data: T) void {
            std.debug.print("Make an Update Statment", .{});
            self.data.items[@intFromEnum(data.id) - 1] = data;
        }

        pub fn get (self: Self, id: T.ID) ?T {
            if (@intFromEnum(id) > self.data.items.len) return null;
            return self.data.items[@intFromEnum(id) - 1];
        }

        pub fn filter(self: Self, filters: anytype, outbuff: []T) []T{
            const filter_data = @typeInfo(@TypeOf(filters));
            var count: usize = 0;
            var i: usize = 0;
            jmp: while (count < outbuff.len or i < self.data.items.len) : (i += 1) {
                const item = self.data.items[i];
                // Comptile Time DuckTyping FTW
                inline for (filter_data.@"struct".fields) |field| {
                    if (@field(item, field.name) != @field(filters, field.name)) {
                        break :jmp;
                    }
                }
                outbuff[count] = item;
                count += 1;
            }
            return outbuff[0..count];
        }
    };
}

3 Likes