From b1f92020822f320ac5ee5b08769c3b30e609fec8 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Mon, 7 Sep 2026 20:44:35 +0200 Subject: [PATCH] feat: add fromByte and encodeTableJoin to protocol --- src/protocol.zig | 146 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) diff --git a/src/protocol.zig b/src/protocol.zig index e6249ca..52fecc4 100644 --- a/src/protocol.zig +++ b/src/protocol.zig @@ -1115,3 +1115,149 @@ pub const JoinMode = enum(u8) { right = 2, full = 3, + pub fn fromByte(b: u8) !JoinMode { + return std.meta.intToEnum(JoinMode, b) catch error.UnknownJoinMode; + } +}; + +/// Wire request for a remote `table join`: which variant (`mode`), the two +/// tables and the fields they are joined on, and an optional `expr.zig` +/// predicate text for each side - the same `leftPred`/`rightPred` filter +/// `join.innerJoin` and its outer variants already take, evaluated exactly +/// like `table_select`'s `exprText` (empty selects every row on that side). +pub fn encodeTableJoin( + allocator: Allocator, + mode: JoinMode, + leftTable: []const u8, + leftField: []const u8, + rightTable: []const u8, + rightField: []const u8, + offset: u32, + limit: u32, + leftExprText: []const u8, + rightExprText: []const u8, +) ![]u8 { + var buf = std.ArrayList(u8).init(allocator); + errdefer buf.deinit(); + const w = encode.ByteWriter.init(&buf); + try w.byte(@intFromEnum(mode)); + try putLenPrefixed(w, leftTable); + try putLenPrefixed(w, leftField); + try putLenPrefixed(w, rightTable); + try putLenPrefixed(w, rightField); + try w.putU32(offset); + try w.putU32(limit); + try putLenPrefixed(w, leftExprText); + try w.slice(rightExprText); + return buf.toOwnedSlice(); +} + +pub const TableJoinArgs = struct { + mode: JoinMode, + leftTable: []const u8, + leftField: []const u8, + rightTable: []const u8, + rightField: []const u8, + offset: u32 = 0, + limit: u32 = 0, + leftExprText: []const u8, + rightExprText: []const u8, +}; + +pub fn decodeTableJoin(payload: []const u8) !TableJoinArgs { + var c = Cursor{ .data = payload }; + const mode = try JoinMode.fromByte(try c.takeByte()); + const leftTable = try c.takeLenPrefixed(); + const leftField = try c.takeLenPrefixed(); + const rightTable = try c.takeLenPrefixed(); + const rightField = try c.takeLenPrefixed(); + const offset = try c.takeU32(); + const limit = try c.takeU32(); + const leftExprText = try c.takeLenPrefixed(); + const rightExprText = c.data[c.pos..]; + return .{ + .mode = mode, + .leftTable = leftTable, + .leftField = leftField, + .rightTable = rightTable, + .rightField = rightField, + .offset = offset, + .limit = limit, + .leftExprText = leftExprText, + .rightExprText = rightExprText, + }; +} + +/// One row pair from any of `join.zig`'s four variants, flattened to a +/// single wire shape: `innerJoin` never leaves either side null, +/// `leftOuterJoin`/`rightOuterJoin` leave exactly one side nullable, and +/// `fullOuterJoin` allows either - rather than four near-identical wire +/// encodings, every variant's result is converted to this one shape before +/// `encodeJoinedRows`, and the CLI decides what "(nil)" means from which +/// `--left`/`--right`/`--full` flag it sent, exactly like the local `table +/// join` printer already does per `JoinMode`. +pub const JoinPair = struct { left: ?rowmod.Row, right: ?rowmod.Row }; + +fn putOptionalRow(allocator: Allocator, w: encode.ByteWriter, row: ?rowmod.Row) !void { + if (row) |*r| { + try w.byte(1); + const encoded = try r.encode(allocator); + defer allocator.free(encoded); + try putLenPrefixed(w, encoded); + } else { + try w.byte(0); + } +} + +pub fn encodeJoinedRows(allocator: Allocator, pairs: []const JoinPair) ![]u8 { + var buf = std.ArrayList(u8).init(allocator); + errdefer buf.deinit(); + const w = encode.ByteWriter.init(&buf); + try w.putU32(@intCast(pairs.len)); + for (pairs) |p| { + try putOptionalRow(allocator, w, p.left); + try putOptionalRow(allocator, w, p.right); + } + return buf.toOwnedSlice(); +} + +fn takeOptionalRow(allocator: Allocator, c: *Cursor) !?rowmod.Row { + const present = try c.takeByte(); + if (present == 0) return null; + const raw = try c.takeLenPrefixed(); + return rowmod.Row.decode(allocator, raw) catch return error.Malformed; +} + +/// Decodes a `table_join` result set into independently owned `JoinPair`s - +/// callers must free each present side with `Row.deinit` (see +/// `freeJoinPairs` below). +pub fn decodeJoinedRows(allocator: Allocator, payload: []const u8) ![]JoinPair { + var c = Cursor{ .data = payload }; + const count = try c.takeU32(); + var list = try std.ArrayList(JoinPair).initCapacity(allocator, count); + errdefer { + for (list.items) |*p| { + if (p.left) |*l| l.deinit(); + if (p.right) |*r| r.deinit(); + } + list.deinit(); + } + var i: u32 = 0; + while (i < count) : (i += 1) { + const left = try takeOptionalRow(allocator, &c); + const right = try takeOptionalRow(allocator, &c); + list.appendAssumeCapacity(.{ .left = left, .right = right }); + } + return list.toOwnedSlice(); +} + +pub fn freeJoinPairs(allocator: Allocator, pairs: []JoinPair) void { + for (pairs) |*p| { + if (p.left) |*l| l.deinit(); + if (p.right) |*r| r.deinit(); + } + allocator.free(pairs); +} + +// --- table aggregate --- +