diff --git a/src/protocol.zig b/src/protocol.zig
index 4596070..883179b 100644
--- a/src/protocol.zig
+++ b/src/protocol.zig
@@ -2036,3 +2036,162 @@ pub fn encodeTableDelete(allocator: Allocator, table: []const u8, exprText: []co
return buf.toOwnedSlice();
}
+pub const TableDeleteArgs = struct { table: []const u8, exprText: []const u8 };
+
+/// `table` and `exprText` are borrowed slices into `payload`, the same
+/// convention `decodeTableExplain`/`decodeTableUpdate` use.
+pub fn decodeTableDelete(payload: []const u8) !TableDeleteArgs {
+ var c = Cursor{ .data = payload };
+ const table = try c.takeLenPrefixed();
+ const exprText = c.data[c.pos..];
+ return .{ .table = table, .exprText = exprText };
+}
+
+// --- table export / table import ---
+
+/// Which interchange format a `table_export`/`table_import` request uses -
+/// the wire counterpart of the CLI's `csv`/`json` string argument (see
+/// `main.zig`'s local `table export`/`table import`), carried as one byte
+/// instead of a length-prefixed string since there are only ever two.
+pub const DumpFormat = enum(u8) {
+ csv = 0,
+ json = 1,
+};
+
+/// Wire request for a remote `table export`: a table name and which format
+/// (`dump.zig`'s `exportCsvWriter`/`exportJsonWriter`) to render it in. No
+/// predicate/paging - export always dumps the whole table, matching the
+/// local CLI's `table export
csv|json `.
+pub fn encodeTableExport(allocator: Allocator, table: []const u8, format: DumpFormat) ![]u8 {
+ var buf = std.ArrayList(u8).init(allocator);
+ errdefer buf.deinit();
+ const w = encode.ByteWriter.init(&buf);
+ try putLenPrefixed(w, table);
+ try w.byte(@intFromEnum(format));
+ return buf.toOwnedSlice();
+}
+
+pub const TableExportArgs = struct { table: []const u8, format: DumpFormat };
+
+pub fn decodeTableExport(payload: []const u8) !TableExportArgs {
+ var c = Cursor{ .data = payload };
+ const table = try c.takeLenPrefixed();
+ const formatByte = try c.takeByte();
+ const format = std.meta.intToEnum(DumpFormat, formatByte) catch return error.Malformed;
+ return .{ .table = table, .format = format };
+}
+
+/// Wire response for `table_export`: the row count `dump.zig`'s exporter
+/// reported (so the CLI can print the same count the local `table export`
+/// prints) followed by the rendered content bytes verbatim - a "count then
+/// rest of the payload" shape, the same idea `exprText` fields elsewhere in
+/// this file use for their own trailing variable-length piece.
+pub fn encodeExportResult(allocator: Allocator, rowCount: u64, content: []const u8) ![]u8 {
+ var buf = std.ArrayList(u8).init(allocator);
+ errdefer buf.deinit();
+ const w = encode.ByteWriter.init(&buf);
+ try w.putU64(rowCount);
+ try w.slice(content);
+ return buf.toOwnedSlice();
+}
+
+pub const ExportResult = struct { rowCount: u64, content: []const u8 };
+
+/// `content` is a borrowed slice into `payload`, the same convention
+/// `decodeTableExplain`'s `exprText` uses - the caller decides whether to
+/// copy it (`client.zig`'s `tableExport` dupes it into an allocation the
+/// caller owns once the response frame itself is about to be freed).
+pub fn decodeExportResult(payload: []const u8) !ExportResult {
+ var c = Cursor{ .data = payload };
+ const rowCount = try c.takeU64();
+ const content = payload[c.pos..];
+ return .{ .rowCount = rowCount, .content = content };
+}
+
+/// Wire request for a remote `table import`: a table name, which format the
+/// following `content` bytes are encoded in, and the content itself.
+/// `content` comes last (decoded as "everything left in the payload", the
+/// same convention every `exprText` field in this file relies on) since it
+/// is arbitrary-length and, unlike `table`, needs no length prefix of its
+/// own.
+pub fn encodeTableImport(allocator: Allocator, table: []const u8, format: DumpFormat, content: []const u8) ![]u8 {
+ var buf = std.ArrayList(u8).init(allocator);
+ errdefer buf.deinit();
+ const w = encode.ByteWriter.init(&buf);
+ try putLenPrefixed(w, table);
+ try w.byte(@intFromEnum(format));
+ try w.slice(content);
+ return buf.toOwnedSlice();
+}
+
+pub const TableImportArgs = struct { table: []const u8, format: DumpFormat, content: []const u8 };
+
+/// `table` and `content` are borrowed slices into `payload`, the same
+/// convention every other decode function in this file uses.
+pub fn decodeTableImport(payload: []const u8) !TableImportArgs {
+ var c = Cursor{ .data = payload };
+ const table = try c.takeLenPrefixed();
+ const formatByte = try c.takeByte();
+ const format = std.meta.intToEnum(DumpFormat, formatByte) catch return error.Malformed;
+ const content = payload[c.pos..];
+ return .{ .table = table, .format = format, .content = content };
+}
+
+// --- stats ---
+
+pub fn encodeStats(allocator: Allocator, stats: dbmod.Stats) ![]u8 {
+ var buf = std.ArrayList(u8).init(allocator);
+ errdefer buf.deinit();
+ const w = encode.ByteWriter.init(&buf);
+ try w.putU64(@intCast(stats.memtableEntries));
+ try w.putU64(@intCast(stats.memtableBytes));
+ try w.putU64(@intCast(stats.sstableCount));
+ try w.putU64(stats.liveEntries);
+ try w.putU64(stats.physicalBytes);
+ try w.putU64(stats.cacheHits);
+ try w.putU64(stats.cacheMisses);
+ try w.putU64(stats.nextSeq);
+ for (stats.levels) |level| {
+ try w.putU64(@intCast(level.tables));
+ try w.putU64(level.entries);
+ try w.putU64(level.physicalBytes);
+ }
+ return buf.toOwnedSlice();
+}
+
+pub fn decodeStats(payload: []const u8) !dbmod.Stats {
+ var c = Cursor{ .data = payload };
+ var stats = dbmod.Stats{};
+ stats.memtableEntries = @intCast(try c.takeU64());
+ stats.memtableBytes = @intCast(try c.takeU64());
+ stats.sstableCount = @intCast(try c.takeU64());
+ stats.liveEntries = try c.takeU64();
+ stats.physicalBytes = try c.takeU64();
+ stats.cacheHits = try c.takeU64();
+ stats.cacheMisses = try c.takeU64();
+ stats.nextSeq = try c.takeU64();
+ var i: usize = 0;
+ while (i < stats.levels.len) : (i += 1) {
+ stats.levels[i] = .{
+ .tables = @intCast(try c.takeU64()),
+ .entries = try c.takeU64(),
+ .physicalBytes = try c.takeU64(),
+ };
+ }
+ return stats;
+}
+
+test "frame round trip through an in-memory buffer" {
+ const allocator = std.testing.allocator;
+ var buf = std.ArrayList(u8).init(allocator);
+ defer buf.deinit();
+
+ try writeFrame(buf.writer(), @intFromEnum(Op.get), "hello");
+ var stream = std.io.fixedBufferStream(buf.items);
+ var frame = try readFrame(allocator, stream.reader());
+ defer frame.deinit();
+
+ try std.testing.expectEqual(@intFromEnum(Op.get), frame.tag);
+ try std.testing.expectEqualStrings("hello", frame.payload);
+}
+