From d09dc6aaf0986a77931e93f49aa8f4d30bb271b4 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Mon, 7 Sep 2026 22:13:44 +0200 Subject: [PATCH] feat: add TableDeleteArgs and decodeTableDelete to protocol --- src/protocol.zig | 159 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) 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); +} +