karlseguin / pg.zig

Native PostgreSQL driver / client for Zig
MIT License
214 stars 16 forks source link

Extend row.get() to handle iterator types, which in turn fixes row.to() to handle iterator columns #23

Closed zigster64 closed 3 months ago

zigster64 commented 3 months ago

The new row.to() function is very useful

Im using a number of Iterator([]const u8) fields in my structs, and I find that this small change to row.to() works for me :

        inline for (std.meta.fields(T)) |field| {
            switch (field.type) {
                Iterator([]const u8) => {
                    @field(result, field.name) = self.iterator([]const u8, columnIndex);
                },
                else => {
                    const value = self.get(field.type, columnIndex);
                    @field(result, field.name) = switch (field.type) {
                        []u8, []const u8 => if (allocator) |a| try a.dupe(u8, value) else value,
                        ?[]u8, ?[]const u8 => blk: {
                            const v = value orelse break :blk null;
                            break :blk if (allocator) |a| try a.dupe(u8, v) else v;
                        },
                        else => value,
                    };
                },
            }
            columnIndex += 1;
        }

I think looking at the more general solution - instead of expanding this out to cover every possible iterator, it might be cleaner to simply extend row.get(type, col) to handle iterator field types.

So row.get(Iterator(T), col) -> returns an Iterator(T)

The code for row.get() and row.iterator() is pretty close anyway - its just the decoder assignment that sets them apart

Doing that would also mean that row.iterator() becomes redundant

karlseguin commented 3 months ago

Do you really have Iterator(T) as your field type, so your struct value is only valid until the next next() ? I'd expect most people would have the slice, like []u32, which would implicitly imply calling alloc on the iterator to create + populate a slice.

I guess both could be supported.

zigster64 commented 3 months ago

yep - my struct has a field of type pg.Iterator(T)

General application of this (for me) is im in a http handler, looping through query.next to get a row, then converting that row directly to HTML output via a template .. then onto the next row.

I never want to be cloning any columns out of the row for what Im currently doing.

This works out fine if you are 100% aware that you are dealing with very limited lifetimes on row values. Sits well with htmx for example. Might sound a bit awkward, but I think its a good performance-first, zero-allocation option.

My usage of this admittedly unusual - I wouldn't expect most people to be doing things this way.

I guess the most common case in web dev would be to slurp the whole SQL result into an array of struct, then either convert that to json for output .... or pass the whole struct + arrays through a template for rendering.

zigster64 commented 3 months ago

Here is an example of what Im doing

So across my app, I wrap an SQL statement + a target struct, then pass that to a query wrapper that gives me a next(T) function that fetches a row, and populates the struct, and returns an instance of that struct

In the case of iterators - I deal with each one immediately on reading the row, and im done with it by the time the next next() is called.

const DB = @import("database.zig");

// A wrapper struct that encapsulates an SQL select statement, and a struct to map each row to
const CustomerList = struct {
    const select_customer_list =
        \\ SELECT
        \\      LOWER(end_user_name) as name,
        \\      COUNT(*) as count,
        \\      SUM(amount) as value,
        \\      ARRAY_AGG(distinct end_user_name) AS name_variations
        \\ FROM purchase_order
        \\ WHERE LOWER(end_user_name) LIKE LOWER('%' || $1 || '%')
        \\ GROUP BY LOWER(end_user_name)
        \\ ORDER BY name
    ;

    results: ?DB.Query = null,

    pub const Row = struct {
        name: []const u8 = "",
        count: i64 = 0,
        value: f64 = 0,
        aliases: pg.Iterator([]const u8) = undefined,
    };

    pub fn select(conn: *pg.Conn, arena: Allocator, params: anytype) !CustomerList {
        return .{
            // DB.Query.select returns a wrapper over a pg.Query 
           // that provides a next() function that takes a struct type to return each row as
            .results = try DB.Query.select(conn, arena, select_customer_list, params),
        };
    }
};

// example usage of the CustomerList wrapper
// render a table of Customers matching part of a string,
// along with a list of aliased misspellings of the name
// from other orders
fn someHandler(...) !void {
    var customers = try CustomerList.select(db, res.arena, .{"matching name"});

    try zts.writeHeader(t, w);
    try zts.write(t, "table-start", w);

    if (customers.results) |*results| {
      // so here, calling results.next() returns a value of type CustomerList.Row
        while (try results.next(CustomerList.Row)) |customer| {
            try zts.print(t, "customer-row", customer, w);

           // one of the struct fields is an iterator 
          // so iterate over it now, whilst we have the row open
            var aliases = customer.aliases;
            while (aliases.next()) |alias| {
                try zts.print(t, "alias", .{ .alias = alias }, w);
            }
            try zts.write(t, "alias-end", w);
        }
        results.deinit();
    }
    try zts.write(t, "table-end", w);
}
karlseguin commented 3 months ago

row.get(pg.Iterator(T), col) is now supported.

The to and mapper behavior will now map an array column to a pg.Iterator(T) field.

In cases where to/mapper is given a custom allocator, deinit(allocator) must be called on the iterator.

zigster64 commented 3 months ago

Thx, that is working for me, all good.

There is a separate memory allocation issue though - will raise a separate issue + patch soon