feat: add deinit and decodeCollReply to protocol

This commit is contained in:
dualfroz committed 2026-09-07 19:46:54 +02:00
1 parent eb441341cb
commit c928cc7302
1 file changed
+152
+152
View File
@@ -649,3 +649,155 @@ pub const OwnedCollReply = union(enum) {
pairList: []OwnedCollPair,
scorePairList: []OwnedCollScorePair,
pub fn deinit(self: *OwnedCollReply, allocator: Allocator) void {
switch (self.*) {
.bytes => |b| allocator.free(b),
.stringList => |items| {
for (items) |it| allocator.free(it);
allocator.free(items);
},
.pairList => |items| {
for (items) |p| {
allocator.free(p.field);
allocator.free(p.value);
}
allocator.free(items);
},
.scorePairList => |items| {
for (items) |p| allocator.free(p.member);
allocator.free(items);
},
.nil, .ok, .int, .real => {},
}
self.* = undefined;
}
};
pub fn decodeCollReply(allocator: Allocator, payload: []const u8) !OwnedCollReply {
var c = Cursor{ .data = payload };
const tag = std.meta.intToEnum(CollReplyTag, try c.takeByte()) catch return error.Malformed;
return switch (tag) {
.nil => .nil,
.ok => .ok,
.int => .{ .int = @bitCast(try c.takeU64()) },
.real => .{ .real = @bitCast(try c.takeU64()) },
.bytes => .{ .bytes = try allocator.dupe(u8, c.data[c.pos..]) },
.stringList => blk: {
const count = try c.takeU32();
var list = try std.ArrayList([]u8).initCapacity(allocator, count);
errdefer {
for (list.items) |it| allocator.free(it);
list.deinit();
}
var i: u32 = 0;
while (i < count) : (i += 1) {
const raw = try c.takeLenPrefixed();
list.appendAssumeCapacity(try allocator.dupe(u8, raw));
}
break :blk .{ .stringList = try list.toOwnedSlice() };
},
.pairList => blk: {
const count = try c.takeU32();
var list = try std.ArrayList(OwnedCollPair).initCapacity(allocator, count);
errdefer {
for (list.items) |p| {
allocator.free(p.field);
allocator.free(p.value);
}
list.deinit();
}
var i: u32 = 0;
while (i < count) : (i += 1) {
const field = try c.takeLenPrefixed();
const value = try c.takeLenPrefixed();
list.appendAssumeCapacity(.{ .field = try allocator.dupe(u8, field), .value = try allocator.dupe(u8, value) });
}
break :blk .{ .pairList = try list.toOwnedSlice() };
},
.scorePairList => blk: {
const count = try c.takeU32();
var list = try std.ArrayList(OwnedCollScorePair).initCapacity(allocator, count);
errdefer {
for (list.items) |p| allocator.free(p.member);
list.deinit();
}
var i: u32 = 0;
while (i < count) : (i += 1) {
const member = try c.takeLenPrefixed();
const score: f64 = @bitCast(try c.takeU64());
list.appendAssumeCapacity(.{ .member = try allocator.dupe(u8, member), .score = score });
}
break :blk .{ .scorePairList = try list.toOwnedSlice() };
},
};
}
// --- txn_op ---
/// Remote counterpart of the CLI's local `txn <script-file>` (main.zig's
/// `runTxn`, backed by `txn.zig`/`txn_table.zig`): a batch of put/delete row
/// operations, possibly spanning several tables, applied as one atomic
/// `Transaction` commit - the same "every op succeeds together or none of
/// them do" guarantee the local CLI gives, now reachable from a process that
/// only has a `Client`, not a `*Db`. Unlike `coll_op`/`ttl_op` (one action
/// against one key/collection per request) this opcode carries a LIST of
/// entries, since a transaction script is inherently a batch.
pub const TxnVerb = enum(u8) {
put = 0,
del = 1,
pub fn fromByte(b: u8) !TxnVerb {
return std.meta.intToEnum(TxnVerb, b) catch error.Malformed;
}
};
/// One staged operation as the caller supplies it. `fields` is ignored for
/// `.del` (callers pass `&.{}`) - same "unused argument still there, just
/// empty" convention as `encodeUserAdmin`'s zeroed `role`/`rounds` on
/// actions that do not need them.
pub const TxnOpEntry = struct {
verb: TxnVerb,
table: []const u8,
pk: rowmod.Value,
fields: []const rowmod.Field = &.{},
};
pub fn encodeTxnOp(allocator: Allocator, entries: []const TxnOpEntry) ![]u8 {
var buf = std.ArrayList(u8).init(allocator);
errdefer buf.deinit();
const w = encode.ByteWriter.init(&buf);
try w.putU32(@intCast(entries.len));
for (entries) |e| {
try w.byte(@intFromEnum(e.verb));
try putLenPrefixed(w, e.table);
const pkBytes = try rowmod.encodeValue(allocator, e.pk);
defer allocator.free(pkBytes);
try putLenPrefixed(w, pkBytes);
// Reuses `Row.encode` for the field list exactly the way
// `encodeRows` does for a whole result set, instead of inventing a
// second name+value framing just for this opcode.
var fieldsRow = rowmod.Row.init(allocator);
defer fieldsRow.deinit();
for (e.fields) |f| try fieldsRow.set(f.name, f.value);
const fieldsBytes = try fieldsRow.encode(allocator);
defer allocator.free(fieldsBytes);
try putLenPrefixed(w, fieldsBytes);
}
return buf.toOwnedSlice();
}
/// Decoded entry with every slice borrowed from the frame `payload` that
/// carried it - same convention as `decodeCollOp`/`decodeTableSelect`, valid
/// only as long as that payload is alive. `pkBytes`/`fieldsBytes` still need
/// `rowmod.decodeValue`/`rowmod.Row.decode` to become a real `Value`/field
/// list; kept undecoded here so a malformed table name or verb byte fails
/// fast without touching row-decoding at all.
pub const DecodedTxnOpEntry = struct {
verb: TxnVerb,
table: []const u8,
pkBytes: []const u8,
fieldsBytes: []const u8,
};