diff --git a/src/server.zig b/src/server.zig
index 8a30e44..935201d 100644
--- a/src/server.zig
+++ b/src/server.zig
@@ -1699,3 +1699,155 @@ pub const Server = struct {
};
defer plan.deinit(self.allocator);
+ const out = protocol.encodePlan(self.allocator, plan) catch |err| {
+ return writeErr(writer, .internal, @errorName(err));
+ };
+ defer self.allocator.free(out);
+ try protocol.writeFrame(writer, @intFromEnum(protocol.Status.ok), out);
+ }
+
+ /// Executes a `table_update` request: the network counterpart of the
+ /// CLI's local `table update
--set ... [expr...]`, running
+ /// `query.Table.updateWhere` against the server's `*Db`. The wire
+ /// `sets` field list arrives pre-encoded with `Row.encode` (the same
+ /// trick `decodeTxnOp`'s `fieldsBytes` uses), so it is decoded into a
+ /// real `Row` here before `updateWhere` ever sees it. The response is a
+ /// bare row count reusing `encodeTtlSweepCount`'s u64 wire shape - the
+ /// same "no new response type, an existing shape already fits"
+ /// precedent `coll_ttl_op` set for `ttl_op`'s result encoding.
+ fn handleTableUpdate(self: *Server, payload: []const u8, writer: anytype) !void {
+ const args = protocol.decodeTableUpdate(payload) catch {
+ return writeErr(writer, .bad_request, "malformed table_update payload");
+ };
+
+ var setsRow = rowmod.Row.decode(self.allocator, args.setsBytes) catch {
+ return writeErr(writer, .bad_request, "malformed table_update set fields");
+ };
+ defer setsRow.deinit();
+
+ var table = querymod.Table.open(self.db, self.allocator, args.table) catch |err| {
+ return writeErr(writer, .bad_request, @errorName(err));
+ };
+ defer table.deinit();
+
+ var parsed: ?exprmod.Parsed = null;
+ defer if (parsed) |*p| p.deinit();
+ if (args.exprText.len > 0) {
+ parsed = exprmod.parse(self.allocator, args.exprText) catch |err| {
+ return writeErr(writer, .bad_request, @errorName(err));
+ };
+ }
+ const pred: ?querymod.Predicate = if (parsed) |p| p.predicate else null;
+
+ const updated = table.updateWhere(pred, setsRow.fields.items) catch |err| {
+ return writeErr(writer, .internal, @errorName(err));
+ };
+
+ const out = protocol.encodeTtlSweepCount(self.allocator, @intCast(updated)) catch |err| {
+ return writeErr(writer, .internal, @errorName(err));
+ };
+ defer self.allocator.free(out);
+ try protocol.writeFrame(writer, @intFromEnum(protocol.Status.ok), out);
+ }
+
+ /// Executes a `table_delete` request: the network counterpart of the
+ /// CLI's local `table delete [expr...]`, running
+ /// `query.Table.deleteWhere` against the server's `*Db`. Same wire shape
+ /// as `table_explain` (table + exprText, no `SET` clause) and same
+ /// response shape as `table_update` (a bare row count reusing
+ /// `encodeTtlSweepCount`'s u64 wire shape).
+ fn handleTableDelete(self: *Server, payload: []const u8, writer: anytype) !void {
+ const args = protocol.decodeTableDelete(payload) catch {
+ return writeErr(writer, .bad_request, "malformed table_delete payload");
+ };
+
+ var table = querymod.Table.open(self.db, self.allocator, args.table) catch |err| {
+ return writeErr(writer, .bad_request, @errorName(err));
+ };
+ defer table.deinit();
+
+ var parsed: ?exprmod.Parsed = null;
+ defer if (parsed) |*p| p.deinit();
+ if (args.exprText.len > 0) {
+ parsed = exprmod.parse(self.allocator, args.exprText) catch |err| {
+ return writeErr(writer, .bad_request, @errorName(err));
+ };
+ }
+ const pred: ?querymod.Predicate = if (parsed) |p| p.predicate else null;
+
+ const deleted = table.deleteWhere(pred) catch |err| {
+ return writeErr(writer, .internal, @errorName(err));
+ };
+
+ const out = protocol.encodeTtlSweepCount(self.allocator, @intCast(deleted)) catch |err| {
+ return writeErr(writer, .internal, @errorName(err));
+ };
+ defer self.allocator.free(out);
+ try protocol.writeFrame(writer, @intFromEnum(protocol.Status.ok), out);
+ }
+
+ /// Executes a `table_export` request: the network counterpart of the
+ /// CLI's local `table export csv|json `, rendering the
+ /// whole table through `dump.zig`'s `exportCsvWriter`/`exportJsonWriter`
+ /// (the same functions the local path calls, just pointed at an
+ /// in-memory `ArrayList` writer instead of a file, since there is no
+ /// filesystem path to write to on the server side of a request) and
+ /// sending the row count plus the rendered bytes back in one frame.
+ fn handleTableExport(self: *Server, payload: []const u8, writer: anytype) !void {
+ const args = protocol.decodeTableExport(payload) catch {
+ return writeErr(writer, .bad_request, "malformed table_export payload");
+ };
+
+ var table = querymod.Table.open(self.db, self.allocator, args.table) catch |err| {
+ return writeErr(writer, .bad_request, @errorName(err));
+ };
+ defer table.deinit();
+
+ var buf = std.ArrayList(u8).init(self.allocator);
+ defer buf.deinit();
+ const rowCount = switch (args.format) {
+ .csv => dumpmod.exportCsvWriter(self.allocator, &table, buf.writer()),
+ .json => dumpmod.exportJsonWriter(self.allocator, &table, buf.writer()),
+ } catch |err| {
+ return writeErr(writer, .internal, @errorName(err));
+ };
+
+ const out = protocol.encodeExportResult(self.allocator, @intCast(rowCount), buf.items) catch |err| {
+ return writeErr(writer, .internal, @errorName(err));
+ };
+ defer self.allocator.free(out);
+ try protocol.writeFrame(writer, @intFromEnum(protocol.Status.ok), out);
+ }
+
+ /// Executes a `table_import` request: the network counterpart of the
+ /// CLI's local `table import csv|json `, feeding the
+ /// wire-carried `content` bytes straight into `dump.zig`'s
+ /// `importCsvContent`/`importJsonContent` (the same functions the local
+ /// path calls after reading a file into memory - the client already did
+ /// that reading, so the server only ever sees bytes, never a path)
+ /// against the server's `*Db`. The response is a bare row count, the
+ /// same u64 wire shape `table_update`/`table_delete` already use.
+ fn handleTableImport(self: *Server, payload: []const u8, writer: anytype) !void {
+ const args = protocol.decodeTableImport(payload) catch {
+ return writeErr(writer, .bad_request, "malformed table_import payload");
+ };
+
+ var table = querymod.Table.open(self.db, self.allocator, args.table) catch |err| {
+ return writeErr(writer, .bad_request, @errorName(err));
+ };
+ defer table.deinit();
+
+ const imported = switch (args.format) {
+ .csv => dumpmod.importCsvContent(self.allocator, &table, args.content),
+ .json => dumpmod.importJsonContent(self.allocator, &table, args.content),
+ } catch |err| {
+ return writeErr(writer, .internal, @errorName(err));
+ };
+
+ const out = protocol.encodeTtlSweepCount(self.allocator, @intCast(imported)) catch |err| {
+ return writeErr(writer, .internal, @errorName(err));
+ };
+ defer self.allocator.free(out);
+ try protocol.writeFrame(writer, @intFromEnum(protocol.Status.ok), out);
+ }
+