From ecec822d5cdb38e833cd8311ce14ef71e2a6429b Mon Sep 17 00:00:00 2001 From: DualFroz Date: Tue, 1 Sep 2026 21:08:57 +0200 Subject: [PATCH] feat: add scan and prefix to client --- src/client.zig | 164 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) diff --git a/src/client.zig b/src/client.zig index 8947e0d..56d836d 100644 --- a/src/client.zig +++ b/src/client.zig @@ -156,3 +156,167 @@ pub const Client = struct { if (status != .ok) return statusToError(status); } + pub fn scan(self: *Client, allocator: Allocator, args: protocol.ScanArgs) ![]protocol.OwnedEntry { + const payload = try protocol.encodeScan(self.allocator, args); + defer self.allocator.free(payload); + var frame = try self.roundTrip(.scan, payload); + defer frame.deinit(); + const status = try protocol.Status.fromByte(frame.tag); + if (status != .ok) return statusToError(status); + return protocol.decodeEntries(allocator, frame.payload); + } + + pub fn prefix(self: *Client, allocator: Allocator, prefixBytes: []const u8, limit: u32) ![]protocol.OwnedEntry { + const payload = try protocol.encodePrefix(self.allocator, prefixBytes, limit); + defer self.allocator.free(payload); + var frame = try self.roundTrip(.prefix, payload); + defer frame.deinit(); + const status = try protocol.Status.fromByte(frame.tag); + if (status != .ok) return statusToError(status); + return protocol.decodeEntries(allocator, frame.payload); + } + + pub fn stats(self: *Client) !dbmod.Stats { + var frame = try self.roundTrip(.stats, ""); + defer frame.deinit(); + const status = try protocol.Status.fromByte(frame.tag); + if (status != .ok) return statusToError(status); + return protocol.decodeStats(frame.payload); + } + + /// Fetches the server's combined Prometheus text export (its own + /// request/rate-limit/auth/session counters from `Server.registerMetrics` + /// plus `Db.stats()`, in the one payload `Server.handleMetrics` builds) - + /// the network counterpart of the CLI's local-only `stats --prom`. + /// Returned text is independently owned; free with `allocator.free`. + pub fn metricsText(self: *Client, allocator: Allocator) ![]u8 { + var frame = try self.roundTrip(.metrics, ""); + defer frame.deinit(); + const status = try protocol.Status.fromByte(frame.tag); + if (status != .ok) return statusToError(status); + return allocator.dupe(u8, frame.payload); + } + + pub fn flush(self: *Client) !void { + var frame = try self.roundTrip(.flush, ""); + defer frame.deinit(); + const status = try protocol.Status.fromByte(frame.tag); + if (status != .ok) return statusToError(status); + } + + pub fn compact(self: *Client) !void { + var frame = try self.roundTrip(.compact, ""); + defer frame.deinit(); + const status = try protocol.Status.fromByte(frame.tag); + if (status != .ok) return statusToError(status); + } + + pub fn batch(self: *Client, ops: []const protocol.BatchOp) !void { + const payload = try protocol.encodeBatch(self.allocator, ops); + defer self.allocator.free(payload); + var frame = try self.roundTrip(.batch, payload); + defer frame.deinit(); + const status = try protocol.Status.fromByte(frame.tag); + if (status != .ok) return statusToError(status); + } + + /// Remote `table explain`: the wire counterpart of the CLI's local + /// `table explain`, reporting which strategy `query.Table.explain` + /// says a matching `tableSelect` would take against the server's + /// `*Db`, without running the query. `exprText` is the same `expr.zig` + /// grammar as `tableSelect`'s (empty means no predicate, i.e. always a + /// full scan). Returned `Plan` owns its `indexName` - free with + /// `Plan.deinit`. + pub fn tableExplain(self: *Client, allocator: Allocator, table: []const u8, exprText: []const u8) !querymod.Plan { + const payload = try protocol.encodeTableExplain(self.allocator, table, exprText); + defer self.allocator.free(payload); + var frame = try self.roundTrip(.table_explain, payload); + defer frame.deinit(); + const status = try protocol.Status.fromByte(frame.tag); + if (status != .ok) return statusToError(status); + return protocol.decodePlan(allocator, frame.payload); + } + + /// Remote `table update`: the wire counterpart of the CLI's local + /// `table update`, applying `sets` to every row matching `exprText` + /// (empty means every row, the same convention `tableSelect`/ + /// `tableExplain` use) against the server's `*Db` via + /// `query.Table.updateWhere`. Returns the number of rows updated. + pub fn tableUpdate(self: *Client, table: []const u8, sets: []const rowmod.Field, exprText: []const u8) !u64 { + const payload = try protocol.encodeTableUpdate(self.allocator, table, sets, exprText); + defer self.allocator.free(payload); + var frame = try self.roundTrip(.table_update, payload); + defer frame.deinit(); + const status = try protocol.Status.fromByte(frame.tag); + if (status != .ok) return statusToError(status); + return protocol.decodeTtlSweepCount(frame.payload); + } + + /// Remote `table delete`: the wire counterpart of the CLI's local + /// `table delete`, removing every row matching `exprText` (empty means + /// every row, the same convention `tableUpdate`/`tableExplain` use) + /// against the server's `*Db` via `query.Table.deleteWhere`. Returns the + /// number of rows deleted. + pub fn tableDelete(self: *Client, table: []const u8, exprText: []const u8) !u64 { + const payload = try protocol.encodeTableDelete(self.allocator, table, exprText); + defer self.allocator.free(payload); + var frame = try self.roundTrip(.table_delete, payload); + defer frame.deinit(); + const status = try protocol.Status.fromByte(frame.tag); + if (status != .ok) return statusToError(status); + return protocol.decodeTtlSweepCount(frame.payload); + } + + /// Remote `table export`: the wire counterpart of the CLI's local + /// `table export csv|json `, rendering the whole table on + /// the server and returning both the row count and the rendered bytes + /// in one round trip. `content` is a fresh allocation independent of + /// the response frame - free it with `RemoteExport.deinit`. + pub fn tableExport(self: *Client, allocator: Allocator, table: []const u8, format: protocol.DumpFormat) !RemoteExport { + const payload = try protocol.encodeTableExport(self.allocator, table, format); + defer self.allocator.free(payload); + var frame = try self.roundTrip(.table_export, payload); + defer frame.deinit(); + const status = try protocol.Status.fromByte(frame.tag); + if (status != .ok) return statusToError(status); + const result = try protocol.decodeExportResult(frame.payload); + const content = try allocator.dupe(u8, result.content); + return .{ .rowCount = result.rowCount, .content = content }; + } + + /// Remote `table import`: the wire counterpart of the CLI's local + /// `table import
csv|json `. `content` is data the + /// caller already read into memory (e.g. from a local file) and sends + /// to the server in one frame; the server runs it through `dump.zig`'s + /// importer there. Returns the number of rows imported. + pub fn tableImport(self: *Client, table: []const u8, format: protocol.DumpFormat, content: []const u8) !u64 { + const payload = try protocol.encodeTableImport(self.allocator, table, format, content); + defer self.allocator.free(payload); + var frame = try self.roundTrip(.table_import, payload); + defer frame.deinit(); + const status = try protocol.Status.fromByte(frame.tag); + if (status != .ok) return statusToError(status); + return protocol.decodeTtlSweepCount(frame.payload); + } + + /// Remote `table select`: `exprText` is the same `expr.zig` predicate + /// grammar the local CLI's `table select` accepts (empty selects every + /// row). `offset`/`limit` page the result the same way + /// `query.Table.selectPage` does locally (0 limit = unlimited), so a + /// large remote result does not have to come back in a single frame. + /// `orderBy`, when non-empty, sorts server-side via + /// `Table.selectPageOrdered` before that page window applies (one or + /// more columns applied left to right, ties on earlier columns broken + /// by later ones) - the network counterpart of the local CLI's `table + /// select --order-by`. Returned rows are independently owned - free + /// with `protocol.freeRows`. + pub fn tableSelect(self: *Client, allocator: Allocator, table: []const u8, exprText: []const u8, offset: u32, limit: u32, orderBy: []const querymod.OrderBy) ![]rowmod.Row { + const payload = try protocol.encodeTableSelect(self.allocator, table, exprText, offset, limit, orderBy); + defer self.allocator.free(payload); + var frame = try self.roundTrip(.table_select, payload); + defer frame.deinit(); + const status = try protocol.Status.fromByte(frame.tag); + if (status != .ok) return statusToError(status); + return protocol.decodeRows(allocator, frame.payload); + } +