1422 lines
52 KiB
Zig
1422 lines
52 KiB
Zig
const std = @import("std");
|
|
const encode = @import("encode.zig");
|
|
const dbmod = @import("db.zig");
|
|
const rowmod = @import("row.zig");
|
|
const authmod = @import("auth.zig");
|
|
const ttlmod = @import("ttl.zig");
|
|
const collmod = @import("collections.zig");
|
|
const aggregatemod = @import("aggregate.zig");
|
|
const windowmod = @import("window.zig");
|
|
const querymod = @import("query.zig");
|
|
|
|
const Allocator = std.mem.Allocator;
|
|
|
|
/// Guard against a corrupt or hostile length prefix causing an unbounded
|
|
/// allocation before a single byte of the frame body has been validated.
|
|
pub const maxFrameBytes: u32 = 64 * 1024 * 1024;
|
|
|
|
pub const Op = enum(u8) {
|
|
ping = 0,
|
|
get = 1,
|
|
set = 2,
|
|
del = 3,
|
|
scan = 4,
|
|
prefix = 5,
|
|
stats = 6,
|
|
flush = 7,
|
|
compact = 8,
|
|
batch = 9,
|
|
table_select = 10,
|
|
auth = 11,
|
|
user_admin = 12,
|
|
auth_token = 13,
|
|
metrics = 14,
|
|
ttl_op = 15,
|
|
coll_ttl_op = 16,
|
|
coll_op = 17,
|
|
txn_op = 18,
|
|
table_join = 19,
|
|
table_aggregate = 20,
|
|
table_explain = 21,
|
|
table_update = 22,
|
|
table_delete = 23,
|
|
table_export = 24,
|
|
table_import = 25,
|
|
table_window = 26,
|
|
subscribe = 27,
|
|
publish = 28,
|
|
unsubscribe = 29,
|
|
|
|
pub fn fromByte(b: u8) !Op {
|
|
return std.meta.intToEnum(Op, b) catch error.UnknownOp;
|
|
}
|
|
};
|
|
|
|
pub const Status = enum(u8) {
|
|
ok = 0,
|
|
not_found = 1,
|
|
bad_request = 2,
|
|
io_error = 3,
|
|
conflict = 4,
|
|
internal = 5,
|
|
rate_limited = 6,
|
|
unauthorized = 7,
|
|
forbidden = 8,
|
|
|
|
pub fn fromByte(b: u8) !Status {
|
|
return std.meta.intToEnum(Status, b) catch error.UnknownStatus;
|
|
}
|
|
};
|
|
|
|
pub const ProtoError = error{
|
|
UnknownOp,
|
|
UnknownStatus,
|
|
FrameTooLarge,
|
|
Malformed,
|
|
};
|
|
|
|
/// A frame is the unit exchanged over the wire in both directions: a
|
|
/// single tag byte (an `Op` on the request side, a `Status` on the
|
|
/// response side) followed by a little-endian u32 payload length and the
|
|
/// payload itself.
|
|
pub const Frame = struct {
|
|
tag: u8,
|
|
payload: []u8,
|
|
allocator: Allocator,
|
|
|
|
pub fn deinit(self: *Frame) void {
|
|
self.allocator.free(self.payload);
|
|
self.* = undefined;
|
|
}
|
|
};
|
|
|
|
pub fn writeFrame(writer: anytype, tag: u8, payload: []const u8) !void {
|
|
if (payload.len > maxFrameBytes) return error.FrameTooLarge;
|
|
var header: [5]u8 = undefined;
|
|
header[0] = tag;
|
|
std.mem.writeInt(u32, header[1..5], @intCast(payload.len), encode.endian);
|
|
try writer.writeAll(&header);
|
|
if (payload.len > 0) try writer.writeAll(payload);
|
|
}
|
|
|
|
pub fn readFrame(allocator: Allocator, reader: anytype) !Frame {
|
|
var header: [5]u8 = undefined;
|
|
try reader.readNoEof(&header);
|
|
const tag = header[0];
|
|
const len = std.mem.readInt(u32, header[1..5], encode.endian);
|
|
if (len > maxFrameBytes) return error.FrameTooLarge;
|
|
const payload = try allocator.alloc(u8, len);
|
|
errdefer allocator.free(payload);
|
|
if (len > 0) try reader.readNoEof(payload);
|
|
return .{ .tag = tag, .payload = payload, .allocator = allocator };
|
|
}
|
|
|
|
/// Bounds-checked cursor over a decoded payload. Every read returns
|
|
/// `error.Malformed` instead of panicking when the peer sent a short or
|
|
/// truncated buffer, since payload bytes come from an untrusted socket.
|
|
pub const Cursor = struct {
|
|
data: []const u8,
|
|
pos: usize = 0,
|
|
|
|
pub fn takeByte(self: *Cursor) !u8 {
|
|
if (self.pos + 1 > self.data.len) return error.Malformed;
|
|
const v = self.data[self.pos];
|
|
self.pos += 1;
|
|
return v;
|
|
}
|
|
|
|
pub fn takeU32(self: *Cursor) !u32 {
|
|
if (self.pos + 4 > self.data.len) return error.Malformed;
|
|
const v = encode.readU32(self.data, self.pos);
|
|
self.pos += 4;
|
|
return v;
|
|
}
|
|
|
|
pub fn takeU64(self: *Cursor) !u64 {
|
|
if (self.pos + 8 > self.data.len) return error.Malformed;
|
|
const v = encode.readU64(self.data, self.pos);
|
|
self.pos += 8;
|
|
return v;
|
|
}
|
|
|
|
pub fn takeBytes(self: *Cursor, n: usize) ![]const u8 {
|
|
if (self.pos + n > self.data.len) return error.Malformed;
|
|
const v = self.data[self.pos .. self.pos + n];
|
|
self.pos += n;
|
|
return v;
|
|
}
|
|
|
|
pub fn takeLenPrefixed(self: *Cursor) ![]const u8 {
|
|
const n = try self.takeU32();
|
|
return self.takeBytes(n);
|
|
}
|
|
|
|
pub fn atEnd(self: *Cursor) bool {
|
|
return self.pos == self.data.len;
|
|
}
|
|
};
|
|
|
|
fn putLenPrefixed(writer: encode.ByteWriter, v: []const u8) !void {
|
|
try writer.putU32(@intCast(v.len));
|
|
try writer.slice(v);
|
|
}
|
|
|
|
// --- set ---
|
|
|
|
pub fn encodeSet(allocator: Allocator, key: []const u8, value: []const u8) ![]u8 {
|
|
var buf = std.ArrayList(u8).init(allocator);
|
|
errdefer buf.deinit();
|
|
const w = encode.ByteWriter.init(&buf);
|
|
try putLenPrefixed(w, key);
|
|
try w.slice(value);
|
|
return buf.toOwnedSlice();
|
|
}
|
|
|
|
pub const SetArgs = struct { key: []const u8, value: []const u8 };
|
|
|
|
pub fn decodeSet(payload: []const u8) !SetArgs {
|
|
var c = Cursor{ .data = payload };
|
|
const key = try c.takeLenPrefixed();
|
|
const value = c.data[c.pos..];
|
|
return .{ .key = key, .value = value };
|
|
}
|
|
|
|
// --- auth ---
|
|
|
|
pub fn encodeAuth(allocator: Allocator, username: []const u8, password: []const u8) ![]u8 {
|
|
var buf = std.ArrayList(u8).init(allocator);
|
|
errdefer buf.deinit();
|
|
const w = encode.ByteWriter.init(&buf);
|
|
try putLenPrefixed(w, username);
|
|
try w.slice(password);
|
|
return buf.toOwnedSlice();
|
|
}
|
|
|
|
pub const AuthArgs = struct { username: []const u8, password: []const u8 };
|
|
|
|
pub fn decodeAuth(payload: []const u8) !AuthArgs {
|
|
var c = Cursor{ .data = payload };
|
|
const username = try c.takeLenPrefixed();
|
|
const password = c.data[c.pos..];
|
|
return .{ .username = username, .password = password };
|
|
}
|
|
|
|
/// A successful `auth` response's payload: empty when the server has no
|
|
/// session store wired up (see server.zig's `enableSessions`), or the raw
|
|
/// session token bytes when it does. There is no separate decode function
|
|
/// on the client side - `frame.payload` itself IS the token (or empty),
|
|
/// no framing needed for a single opaque blob.
|
|
pub fn encodeAuthOk(allocator: Allocator, token: ?[]const u8) ![]u8 {
|
|
return allocator.dupe(u8, token orelse "");
|
|
}
|
|
|
|
// --- auth_token ---
|
|
|
|
/// Reconnects a fresh connection to a role without presenting the
|
|
/// password again, by presenting a token previously handed out in an
|
|
/// `auth` response (see `session.zig`'s `SessionStore.issue`). The
|
|
/// payload is nothing but the opaque token bytes - no framing needed for
|
|
/// a single blob, exactly like `encodeAuthOk`.
|
|
pub fn encodeAuthToken(allocator: Allocator, token: []const u8) ![]u8 {
|
|
return allocator.dupe(u8, token);
|
|
}
|
|
|
|
pub fn decodeAuthToken(payload: []const u8) []const u8 {
|
|
return payload;
|
|
}
|
|
|
|
// --- user_admin ---
|
|
|
|
/// Remote counterpart of the CLI's local `user add|passwd|role|del|list`,
|
|
/// carried as one opcode so `server.zig`'s `requiredRole` can gate all five
|
|
/// actions behind a single `admin`-only check instead of five separate
|
|
/// opcodes. `role`/`rounds` are only meaningful for the actions that use
|
|
/// them (`create` reads both, `set_role` reads only `role`, `set_password`
|
|
/// reads only `rounds`); callers send 0 for the fields an action ignores.
|
|
pub const UserAdminAction = enum(u8) {
|
|
create = 0,
|
|
set_password = 1,
|
|
set_role = 2,
|
|
delete = 3,
|
|
list = 4,
|
|
|
|
pub fn fromByte(b: u8) !UserAdminAction {
|
|
return std.meta.intToEnum(UserAdminAction, b) catch error.Malformed;
|
|
}
|
|
};
|
|
|
|
pub fn encodeUserAdmin(allocator: Allocator, action: UserAdminAction, username: []const u8, password: []const u8, role: authmod.Role, rounds: u32) ![]u8 {
|
|
var buf = std.ArrayList(u8).init(allocator);
|
|
errdefer buf.deinit();
|
|
const w = encode.ByteWriter.init(&buf);
|
|
try w.byte(@intFromEnum(action));
|
|
try w.byte(@intFromEnum(role));
|
|
try w.putU32(rounds);
|
|
try putLenPrefixed(w, username);
|
|
try w.slice(password);
|
|
return buf.toOwnedSlice();
|
|
}
|
|
|
|
pub const UserAdminArgs = struct {
|
|
action: UserAdminAction,
|
|
roleByte: u8,
|
|
rounds: u32,
|
|
username: []const u8,
|
|
password: []const u8,
|
|
};
|
|
|
|
pub fn decodeUserAdmin(payload: []const u8) !UserAdminArgs {
|
|
var c = Cursor{ .data = payload };
|
|
const action = try UserAdminAction.fromByte(try c.takeByte());
|
|
const roleByte = try c.takeByte();
|
|
const rounds = try c.takeU32();
|
|
const username = try c.takeLenPrefixed();
|
|
const password = c.data[c.pos..];
|
|
return .{ .action = action, .roleByte = roleByte, .rounds = rounds, .username = username, .password = password };
|
|
}
|
|
|
|
/// Encodes the response of a `list` user_admin request: every username
|
|
/// paired with its role byte. Reuses `authmod.UserInfo`/`freeUserList`
|
|
/// rather than inventing a parallel type, the same way `encodeStats` reuses
|
|
/// `dbmod.Stats` and `encodeRows` reuses `rowmod.Row`.
|
|
pub fn encodeUserList(allocator: Allocator, users: []const authmod.UserInfo) ![]u8 {
|
|
var buf = std.ArrayList(u8).init(allocator);
|
|
errdefer buf.deinit();
|
|
const w = encode.ByteWriter.init(&buf);
|
|
try w.putU32(@intCast(users.len));
|
|
for (users) |u| {
|
|
try putLenPrefixed(w, u.username);
|
|
try w.byte(@intFromEnum(u.role));
|
|
}
|
|
return buf.toOwnedSlice();
|
|
}
|
|
|
|
/// Decodes a user list response into independently owned entries - free
|
|
/// with `authmod.freeUserList`.
|
|
pub fn decodeUserList(allocator: Allocator, payload: []const u8) ![]authmod.UserInfo {
|
|
var c = Cursor{ .data = payload };
|
|
const count = try c.takeU32();
|
|
var list = try std.ArrayList(authmod.UserInfo).initCapacity(allocator, count);
|
|
errdefer {
|
|
for (list.items) |u| u.deinit(allocator);
|
|
list.deinit();
|
|
}
|
|
var i: u32 = 0;
|
|
while (i < count) : (i += 1) {
|
|
const uname = try c.takeLenPrefixed();
|
|
const role = authmod.Role.fromByte(try c.takeByte()) catch return error.Malformed;
|
|
const owned = try allocator.dupe(u8, uname);
|
|
list.appendAssumeCapacity(.{ .username = owned, .role = role });
|
|
}
|
|
return list.toOwnedSlice();
|
|
}
|
|
|
|
// --- ttl_op ---
|
|
|
|
/// Remote counterpart of the CLI's local `expire`/`persist`/`ttl`/
|
|
/// `expire-sweep` (main.zig, backed by `ttl.zig`), carried as one opcode the
|
|
/// same way `user_admin` bundles five user-management actions under one
|
|
/// opcode - see server.zig's `requiredRole`, which (unlike `user_admin`'s
|
|
/// blanket `admin` gate) inspects this action byte: `query` is read-only
|
|
/// and needs only `readonly`; `expire`/`persist`/`sweep` all mutate (sweep
|
|
/// deletes every due key) and need `readwrite`, matching every other write
|
|
/// opcode.
|
|
pub const TtlAction = enum(u8) {
|
|
expire = 0,
|
|
persist = 1,
|
|
query = 2,
|
|
sweep = 3,
|
|
|
|
pub fn fromByte(b: u8) !TtlAction {
|
|
return std.meta.intToEnum(TtlAction, b) catch error.Malformed;
|
|
}
|
|
};
|
|
|
|
/// `seconds` is only meaningful for `expire` - callers send 0 for the other
|
|
/// three actions, same "unused fields carry a zero" convention as
|
|
/// `encodeUserAdmin`'s `role`/`rounds`. `key` is empty for `sweep`, which
|
|
/// operates on every due key in the store rather than one specific key.
|
|
pub fn encodeTtlOp(allocator: Allocator, action: TtlAction, key: []const u8, seconds: u64) ![]u8 {
|
|
var buf = std.ArrayList(u8).init(allocator);
|
|
errdefer buf.deinit();
|
|
const w = encode.ByteWriter.init(&buf);
|
|
try w.byte(@intFromEnum(action));
|
|
try w.putU64(seconds);
|
|
try w.slice(key);
|
|
return buf.toOwnedSlice();
|
|
}
|
|
|
|
pub const TtlOpArgs = struct { action: TtlAction, seconds: u64, key: []const u8 };
|
|
|
|
pub fn decodeTtlOp(payload: []const u8) !TtlOpArgs {
|
|
var c = Cursor{ .data = payload };
|
|
const action = try TtlAction.fromByte(try c.takeByte());
|
|
const seconds = try c.takeU64();
|
|
const key = c.data[c.pos..];
|
|
return .{ .action = action, .seconds = seconds, .key = key };
|
|
}
|
|
|
|
/// Encodes the response to a `query` ttl_op: reuses `ttlmod.TtlResult`
|
|
/// rather than inventing a parallel type, the same way `encodeStats` reuses
|
|
/// `dbmod.Stats` and `encodeUserList` reuses `authmod.UserInfo`.
|
|
pub fn encodeTtlResult(allocator: Allocator, result: ttlmod.TtlResult) ![]u8 {
|
|
var buf = std.ArrayList(u8).init(allocator);
|
|
errdefer buf.deinit();
|
|
const w = encode.ByteWriter.init(&buf);
|
|
switch (result) {
|
|
.noKey => try w.byte(0),
|
|
.noExpiry => try w.byte(1),
|
|
.seconds => |s| {
|
|
try w.byte(2);
|
|
try w.putU64(s);
|
|
},
|
|
}
|
|
return buf.toOwnedSlice();
|
|
}
|
|
|
|
pub fn decodeTtlResult(payload: []const u8) !ttlmod.TtlResult {
|
|
var c = Cursor{ .data = payload };
|
|
const kind = try c.takeByte();
|
|
return switch (kind) {
|
|
0 => .noKey,
|
|
1 => .noExpiry,
|
|
2 => .{ .seconds = try c.takeU64() },
|
|
else => error.Malformed,
|
|
};
|
|
}
|
|
|
|
/// Encodes the response to a `sweep` ttl_op: how many due keys were reaped.
|
|
pub fn encodeTtlSweepCount(allocator: Allocator, count: u64) ![]u8 {
|
|
var buf = std.ArrayList(u8).init(allocator);
|
|
errdefer buf.deinit();
|
|
const w = encode.ByteWriter.init(&buf);
|
|
try w.putU64(count);
|
|
return buf.toOwnedSlice();
|
|
}
|
|
|
|
pub fn decodeTtlSweepCount(payload: []const u8) !u64 {
|
|
var c = Cursor{ .data = payload };
|
|
return c.takeU64();
|
|
}
|
|
|
|
// --- coll_ttl_op ---
|
|
|
|
/// Remote counterpart of the CLI's local `coll <kind> expire|ttl|persist
|
|
/// <name>`/`coll expire-sweep` (main.zig, backed by `collttl.zig`), same
|
|
/// bundling shape as `ttl_op` above but for a whole typed collection
|
|
/// (Counter/Hash/Set/List/ZSet) instead of one flat key - so the payload
|
|
/// carries an extra `Kind` tag byte the flat version does not need.
|
|
/// Reuses `TtlAction` directly rather than a parallel enum: the four
|
|
/// actions (expire/persist/query/sweep) mean exactly the same thing here,
|
|
/// just against `collttl.zig` instead of `ttl.zig`.
|
|
pub fn encodeCollTtlOp(allocator: Allocator, action: TtlAction, kind: collmod.Kind, name: []const u8, seconds: u64) ![]u8 {
|
|
var buf = std.ArrayList(u8).init(allocator);
|
|
errdefer buf.deinit();
|
|
const w = encode.ByteWriter.init(&buf);
|
|
try w.byte(@intFromEnum(action));
|
|
// `sweep` operates across every kind at once (collttl.sweepExpired
|
|
// scans the whole marker namespace, not one kind's slice of it), so
|
|
// the kind byte is meaningless for that action - callers pass any
|
|
// valid `Kind` and `decodeCollTtlOp` still decodes it, but
|
|
// `handleCollTtlOp` never reads it for `.sweep`. Same "unused field
|
|
// still round-trips" convention as `encodeTtlOp`'s zeroed `seconds`
|
|
// on non-`expire` actions.
|
|
try w.byte(kind.tagByte());
|
|
try w.putU64(seconds);
|
|
try w.slice(name);
|
|
return buf.toOwnedSlice();
|
|
}
|
|
|
|
pub const CollTtlOpArgs = struct { action: TtlAction, kind: collmod.Kind, seconds: u64, name: []const u8 };
|
|
|
|
pub fn decodeCollTtlOp(payload: []const u8) !CollTtlOpArgs {
|
|
var c = Cursor{ .data = payload };
|
|
const action = try TtlAction.fromByte(try c.takeByte());
|
|
const kind = collmod.Kind.fromTagByte(try c.takeByte()) orelse return error.Malformed;
|
|
const seconds = try c.takeU64();
|
|
const name = c.data[c.pos..];
|
|
return .{ .action = action, .kind = kind, .seconds = seconds, .name = name };
|
|
}
|
|
|
|
// --- coll_op ---
|
|
|
|
/// Remote counterpart of the CLI's local `coll <kind> <op> ...` (main.zig's
|
|
/// `runColl`, backed by `collections.zig`) - until now the Redis-like
|
|
/// collections (Counter/Hash/Set/List/ZSet) were reachable only from a
|
|
/// process that had the `Db` open directly, the same gap `ttl_op`/
|
|
/// `coll_ttl_op` already closed for TTL management. One opcode bundles
|
|
/// every collection operation (the same "one opcode, many actions" shape
|
|
/// as `user_admin` and `ttl_op`) rather than one opcode per op, since the
|
|
/// five kinds share almost nothing about their argument count or type.
|
|
/// The payload carries only the action tag, the collection name, and a
|
|
/// variable list of string arguments in the exact CLI text form `runColl`
|
|
/// already parses locally (values/members/indices/scores) - so
|
|
/// `handleCollOp` (server.zig) can reuse the very same
|
|
/// `std.fmt.parseInt`/`parseFloat` calls instead of inventing a second
|
|
/// binary number encoding just for this opcode.
|
|
pub const CollAction = enum(u8) {
|
|
counter_get = 0,
|
|
counter_set = 1,
|
|
counter_incrby = 2,
|
|
hash_set = 3,
|
|
hash_get = 4,
|
|
hash_del = 5,
|
|
hash_len = 6,
|
|
hash_getall = 7,
|
|
set_add = 8,
|
|
set_rem = 9,
|
|
set_ismember = 10,
|
|
set_card = 11,
|
|
set_members = 12,
|
|
// `set_inter`/`set_union`/`set_diff`: `name` is the first set, `args`
|
|
// are every remaining set name - the same split `runColl` already uses
|
|
// (`args[0]` vs `args[1..]`) for these three multi-set operations.
|
|
set_inter = 13,
|
|
set_union = 14,
|
|
set_diff = 15,
|
|
list_lpush = 16,
|
|
list_rpush = 17,
|
|
list_lpop = 18,
|
|
list_rpop = 19,
|
|
list_len = 20,
|
|
list_range = 21,
|
|
zset_add = 22,
|
|
zset_score = 23,
|
|
zset_incrby = 24,
|
|
zset_rem = 25,
|
|
zset_card = 26,
|
|
zset_rangebyrank = 27,
|
|
zset_rangebyscore = 28,
|
|
zset_rank = 29,
|
|
|
|
pub fn fromByte(b: u8) !CollAction {
|
|
return std.meta.intToEnum(CollAction, b) catch error.Malformed;
|
|
}
|
|
|
|
/// Which `collections.Kind` namespace this action's lazy TTL check
|
|
/// (`collttl.expireIfDue`, called on `name` before every dispatch in
|
|
/// `handleCollOp`) and `requiredRole` gating both need - grouped by the
|
|
/// action's numeric range rather than a second explicit table, since
|
|
/// the enum above is itself laid out kind-by-kind.
|
|
pub fn kind(self: CollAction) collmod.Kind {
|
|
return switch (self) {
|
|
.counter_get, .counter_set, .counter_incrby => .counter,
|
|
.hash_set, .hash_get, .hash_del, .hash_len, .hash_getall => .hash,
|
|
.set_add, .set_rem, .set_ismember, .set_card, .set_members, .set_inter, .set_union, .set_diff => .set,
|
|
.list_lpush, .list_rpush, .list_lpop, .list_rpop, .list_len, .list_range => .list,
|
|
.zset_add, .zset_score, .zset_incrby, .zset_rem, .zset_card, .zset_rangebyrank, .zset_rangebyscore, .zset_rank => .zset,
|
|
};
|
|
}
|
|
|
|
/// True for actions that only read - see `server.zig`'s `requiredRole`,
|
|
/// which needs this to decide `readonly` vs `readwrite` from the action
|
|
/// byte alone, the same action-byte peek `ttl_op`/`coll_ttl_op` already
|
|
/// use for their own bundled actions.
|
|
pub fn isReadOnly(self: CollAction) bool {
|
|
return switch (self) {
|
|
.counter_get, .hash_get, .hash_len, .hash_getall, .set_ismember, .set_card, .set_members, .set_inter, .set_union, .set_diff, .list_len, .list_range, .zset_score, .zset_card, .zset_rangebyrank, .zset_rangebyscore, .zset_rank => true,
|
|
else => false,
|
|
};
|
|
}
|
|
};
|
|
|
|
pub fn encodeCollOp(allocator: Allocator, action: CollAction, name: []const u8, args: []const []const u8) ![]u8 {
|
|
var buf = std.ArrayList(u8).init(allocator);
|
|
errdefer buf.deinit();
|
|
const w = encode.ByteWriter.init(&buf);
|
|
try w.byte(@intFromEnum(action));
|
|
try putLenPrefixed(w, name);
|
|
try w.putU32(@intCast(args.len));
|
|
for (args) |a| try putLenPrefixed(w, a);
|
|
return buf.toOwnedSlice();
|
|
}
|
|
|
|
pub const CollOpArgs = struct {
|
|
action: CollAction,
|
|
name: []const u8,
|
|
args: [][]const u8,
|
|
};
|
|
|
|
/// `name` and every entry of `args` are borrowed slices into `payload`,
|
|
/// valid only as long as the frame that carried them is alive - the same
|
|
/// convention `decodeBatch`/`decodeTableSelect` use. Only the `args`
|
|
/// pointer array itself is owned by the caller (free with
|
|
/// `allocator.free`); never its contents.
|
|
pub fn decodeCollOp(allocator: Allocator, payload: []const u8) !CollOpArgs {
|
|
var c = Cursor{ .data = payload };
|
|
const action = try CollAction.fromByte(try c.takeByte());
|
|
const name = try c.takeLenPrefixed();
|
|
const count = try c.takeU32();
|
|
var list = try std.ArrayList([]const u8).initCapacity(allocator, count);
|
|
errdefer list.deinit();
|
|
var i: u32 = 0;
|
|
while (i < count) : (i += 1) {
|
|
list.appendAssumeCapacity(try c.takeLenPrefixed());
|
|
}
|
|
return .{ .action = action, .name = name, .args = try list.toOwnedSlice() };
|
|
}
|
|
|
|
/// One reply shape covers every `coll_op` action, tagged by the first byte -
|
|
/// the response-side counterpart of the request-side `CollAction`. `.nil`
|
|
/// is the wire form of the CLI's `(nil)` (a hash field/list pop/zset score
|
|
/// that does not exist); `.ok` is a bare acknowledgement (counter set,
|
|
/// hash set/del as a flag would double up with `.int`, so those still use
|
|
/// `.int` with 0/1 - see `encodeCollReply`).
|
|
pub const CollReplyTag = enum(u8) {
|
|
nil = 0,
|
|
ok = 1,
|
|
int = 2,
|
|
real = 3,
|
|
bytes = 4,
|
|
stringList = 5,
|
|
pairList = 6,
|
|
scorePairList = 7,
|
|
};
|
|
|
|
pub const CollPair = struct { field: []const u8, value: []const u8 };
|
|
pub const CollScorePair = struct { member: []const u8, score: f64 };
|
|
|
|
pub const CollReply = union(enum) {
|
|
nil,
|
|
ok,
|
|
int: i64,
|
|
real: f64,
|
|
bytes: []const u8,
|
|
stringList: []const []const u8,
|
|
pairList: []const CollPair,
|
|
scorePairList: []const CollScorePair,
|
|
};
|
|
|
|
pub fn encodeCollReply(allocator: Allocator, reply: CollReply) ![]u8 {
|
|
var buf = std.ArrayList(u8).init(allocator);
|
|
errdefer buf.deinit();
|
|
const w = encode.ByteWriter.init(&buf);
|
|
switch (reply) {
|
|
.nil => try w.byte(@intFromEnum(CollReplyTag.nil)),
|
|
.ok => try w.byte(@intFromEnum(CollReplyTag.ok)),
|
|
.int => |v| {
|
|
try w.byte(@intFromEnum(CollReplyTag.int));
|
|
try w.putU64(@bitCast(v));
|
|
},
|
|
.real => |v| {
|
|
try w.byte(@intFromEnum(CollReplyTag.real));
|
|
try w.putU64(@bitCast(v));
|
|
},
|
|
.bytes => |v| {
|
|
try w.byte(@intFromEnum(CollReplyTag.bytes));
|
|
try w.slice(v);
|
|
},
|
|
.stringList => |items| {
|
|
try w.byte(@intFromEnum(CollReplyTag.stringList));
|
|
try w.putU32(@intCast(items.len));
|
|
for (items) |it| try putLenPrefixed(w, it);
|
|
},
|
|
.pairList => |items| {
|
|
try w.byte(@intFromEnum(CollReplyTag.pairList));
|
|
try w.putU32(@intCast(items.len));
|
|
for (items) |p| {
|
|
try putLenPrefixed(w, p.field);
|
|
try putLenPrefixed(w, p.value);
|
|
}
|
|
},
|
|
.scorePairList => |items| {
|
|
try w.byte(@intFromEnum(CollReplyTag.scorePairList));
|
|
try w.putU32(@intCast(items.len));
|
|
for (items) |p| {
|
|
try putLenPrefixed(w, p.member);
|
|
try w.putU64(@bitCast(p.score));
|
|
}
|
|
},
|
|
}
|
|
return buf.toOwnedSlice();
|
|
}
|
|
|
|
pub const OwnedCollPair = struct { field: []u8, value: []u8 };
|
|
pub const OwnedCollScorePair = struct { member: []u8, score: f64 };
|
|
|
|
/// Decoded, independently owned counterpart of `CollReply` - every byte
|
|
/// slice is a fresh allocation the caller must free via `deinit`, unlike
|
|
/// `CollOpArgs` above which stays borrowed (that side is consumed
|
|
/// immediately inside `dispatch` before the frame is freed; this side
|
|
/// crosses back out to a `Client` caller that keeps it around).
|
|
pub const OwnedCollReply = union(enum) {
|
|
nil,
|
|
ok,
|
|
int: i64,
|
|
real: f64,
|
|
bytes: []u8,
|
|
stringList: [][]u8,
|
|
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,
|
|
};
|
|
|
|
pub fn decodeTxnOp(allocator: Allocator, payload: []const u8) ![]DecodedTxnOpEntry {
|
|
var c = Cursor{ .data = payload };
|
|
const count = try c.takeU32();
|
|
var list = try std.ArrayList(DecodedTxnOpEntry).initCapacity(allocator, count);
|
|
errdefer list.deinit();
|
|
var i: u32 = 0;
|
|
while (i < count) : (i += 1) {
|
|
const verb = try TxnVerb.fromByte(try c.takeByte());
|
|
const table = try c.takeLenPrefixed();
|
|
const pkBytes = try c.takeLenPrefixed();
|
|
const fieldsBytes = try c.takeLenPrefixed();
|
|
list.appendAssumeCapacity(.{ .verb = verb, .table = table, .pkBytes = pkBytes, .fieldsBytes = fieldsBytes });
|
|
}
|
|
return list.toOwnedSlice();
|
|
}
|
|
|
|
/// Encodes the response to a successful `txn_op`: how many operations were
|
|
/// committed - always every entry in the request, since a partial commit is
|
|
/// impossible (`error.Conflict` aborts the whole transaction instead, and
|
|
/// surfaces as `Status.conflict` with no payload, the same shape as
|
|
/// `user_admin`'s `UserExists` conflict).
|
|
pub fn encodeTxnResult(allocator: Allocator, applied: u32) ![]u8 {
|
|
var buf = std.ArrayList(u8).init(allocator);
|
|
errdefer buf.deinit();
|
|
const w = encode.ByteWriter.init(&buf);
|
|
try w.putU32(applied);
|
|
return buf.toOwnedSlice();
|
|
}
|
|
|
|
pub fn decodeTxnResult(payload: []const u8) !u32 {
|
|
var c = Cursor{ .data = payload };
|
|
return c.takeU32();
|
|
}
|
|
|
|
// --- scan / prefix ---
|
|
|
|
pub const ScanArgs = struct {
|
|
start: ?[]const u8 = null,
|
|
end: ?[]const u8 = null,
|
|
limit: u32 = 0,
|
|
reverse: bool = false,
|
|
};
|
|
|
|
const scanFlagHasStart: u8 = 1 << 0;
|
|
const scanFlagHasEnd: u8 = 1 << 1;
|
|
const scanFlagReverse: u8 = 1 << 2;
|
|
|
|
pub fn encodeScan(allocator: Allocator, args: ScanArgs) ![]u8 {
|
|
var buf = std.ArrayList(u8).init(allocator);
|
|
errdefer buf.deinit();
|
|
const w = encode.ByteWriter.init(&buf);
|
|
var flags: u8 = 0;
|
|
if (args.start != null) flags |= scanFlagHasStart;
|
|
if (args.end != null) flags |= scanFlagHasEnd;
|
|
if (args.reverse) flags |= scanFlagReverse;
|
|
try w.byte(flags);
|
|
if (args.start) |s| try putLenPrefixed(w, s);
|
|
if (args.end) |e| try putLenPrefixed(w, e);
|
|
try w.putU32(args.limit);
|
|
return buf.toOwnedSlice();
|
|
}
|
|
|
|
pub fn decodeScan(payload: []const u8) !ScanArgs {
|
|
var c = Cursor{ .data = payload };
|
|
const flags = try c.takeByte();
|
|
var args = ScanArgs{};
|
|
if (flags & scanFlagHasStart != 0) args.start = try c.takeLenPrefixed();
|
|
if (flags & scanFlagHasEnd != 0) args.end = try c.takeLenPrefixed();
|
|
args.reverse = flags & scanFlagReverse != 0;
|
|
args.limit = try c.takeU32();
|
|
return args;
|
|
}
|
|
|
|
pub fn encodePrefix(allocator: Allocator, prefix: []const u8, limit: u32) ![]u8 {
|
|
var buf = std.ArrayList(u8).init(allocator);
|
|
errdefer buf.deinit();
|
|
const w = encode.ByteWriter.init(&buf);
|
|
try w.putU32(limit);
|
|
try w.slice(prefix);
|
|
return buf.toOwnedSlice();
|
|
}
|
|
|
|
pub const PrefixArgs = struct { prefix: []const u8, limit: u32 };
|
|
|
|
pub fn decodePrefix(payload: []const u8) !PrefixArgs {
|
|
var c = Cursor{ .data = payload };
|
|
const limit = try c.takeU32();
|
|
const prefix = c.data[c.pos..];
|
|
return .{ .prefix = prefix, .limit = limit };
|
|
}
|
|
|
|
// --- entry lists (scan / prefix responses) ---
|
|
|
|
pub fn encodeEntries(allocator: Allocator, entries: []const dbmod.Entry) ![]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 putLenPrefixed(w, e.key);
|
|
try putLenPrefixed(w, e.value);
|
|
}
|
|
return buf.toOwnedSlice();
|
|
}
|
|
|
|
pub const OwnedEntry = struct {
|
|
key: []u8,
|
|
value: []u8,
|
|
|
|
pub fn deinit(self: *OwnedEntry, allocator: Allocator) void {
|
|
allocator.free(self.key);
|
|
allocator.free(self.value);
|
|
}
|
|
};
|
|
|
|
/// Copies every key and value out of `payload` so the caller can free
|
|
/// the frame that carried them and keep the entries alive independently.
|
|
pub fn decodeEntries(allocator: Allocator, payload: []const u8) ![]OwnedEntry {
|
|
var c = Cursor{ .data = payload };
|
|
const count = try c.takeU32();
|
|
var list = try std.ArrayList(OwnedEntry).initCapacity(allocator, count);
|
|
errdefer {
|
|
for (list.items) |*e| e.deinit(allocator);
|
|
list.deinit();
|
|
}
|
|
var i: u32 = 0;
|
|
while (i < count) : (i += 1) {
|
|
const key = try c.takeLenPrefixed();
|
|
const value = try c.takeLenPrefixed();
|
|
const ownedKey = try allocator.dupe(u8, key);
|
|
errdefer allocator.free(ownedKey);
|
|
const ownedValue = try allocator.dupe(u8, value);
|
|
list.appendAssumeCapacity(.{ .key = ownedKey, .value = ownedValue });
|
|
}
|
|
return list.toOwnedSlice();
|
|
}
|
|
|
|
pub fn freeOwnedEntries(allocator: Allocator, entries: []OwnedEntry) void {
|
|
for (entries) |*e| e.deinit(allocator);
|
|
allocator.free(entries);
|
|
}
|
|
|
|
// --- batch ---
|
|
|
|
pub const BatchOp = union(enum) {
|
|
put: struct { key: []const u8, value: []const u8 },
|
|
del: []const u8,
|
|
};
|
|
|
|
const batchKindPut: u8 = 0;
|
|
const batchKindDel: u8 = 1;
|
|
|
|
pub fn encodeBatch(allocator: Allocator, ops: []const BatchOp) ![]u8 {
|
|
var buf = std.ArrayList(u8).init(allocator);
|
|
errdefer buf.deinit();
|
|
const w = encode.ByteWriter.init(&buf);
|
|
try w.putU32(@intCast(ops.len));
|
|
for (ops) |op| {
|
|
switch (op) {
|
|
.put => |kv| {
|
|
try w.byte(batchKindPut);
|
|
try putLenPrefixed(w, kv.key);
|
|
try putLenPrefixed(w, kv.value);
|
|
},
|
|
.del => |key| {
|
|
try w.byte(batchKindDel);
|
|
try putLenPrefixed(w, key);
|
|
},
|
|
}
|
|
}
|
|
return buf.toOwnedSlice();
|
|
}
|
|
|
|
pub const DecodedBatchOp = union(enum) {
|
|
put: struct { key: []const u8, value: []const u8 },
|
|
del: []const u8,
|
|
};
|
|
|
|
pub fn decodeBatch(allocator: Allocator, payload: []const u8) ![]DecodedBatchOp {
|
|
var c = Cursor{ .data = payload };
|
|
const count = try c.takeU32();
|
|
var list = try std.ArrayList(DecodedBatchOp).initCapacity(allocator, count);
|
|
errdefer list.deinit();
|
|
var i: u32 = 0;
|
|
while (i < count) : (i += 1) {
|
|
const kind = try c.takeByte();
|
|
switch (kind) {
|
|
batchKindPut => {
|
|
const key = try c.takeLenPrefixed();
|
|
const value = try c.takeLenPrefixed();
|
|
list.appendAssumeCapacity(.{ .put = .{ .key = key, .value = value } });
|
|
},
|
|
batchKindDel => {
|
|
const key = try c.takeLenPrefixed();
|
|
list.appendAssumeCapacity(.{ .del = key });
|
|
},
|
|
else => return error.Malformed,
|
|
}
|
|
}
|
|
return list.toOwnedSlice();
|
|
}
|
|
|
|
// --- table select ---
|
|
|
|
/// Wire request for a remote `table select`: a table name, the raw text of
|
|
/// an `expr.zig` predicate expression (empty text selects every row, the
|
|
/// same convention `query.Table.select(null)` uses locally), an
|
|
/// offset/limit page window using the same "0 = unlimited" convention as
|
|
/// `ScanArgs.limit` above - so a large remote result can be paged instead
|
|
/// of always coming back in one frame bounded only by `maxFrameBytes` - and
|
|
/// a `query.OrderBy` sort key list (possibly empty) applied server-side via
|
|
/// `Table.selectPageOrdered` before that page window. The order-by count
|
|
/// byte (and each field name/direction pair behind it) sit BEFORE
|
|
/// `exprText` rather than after it, because `exprText` is decoded as
|
|
/// "everything left in the payload" - anything meant to survive decoding
|
|
/// has to come first. A single byte bounds the count to 255 columns, far
|
|
/// more than any real query needs.
|
|
pub fn encodeTableSelect(allocator: Allocator, table: []const u8, exprText: []const u8, offset: u32, limit: u32, orderBy: []const querymod.OrderBy) ![]u8 {
|
|
var buf = std.ArrayList(u8).init(allocator);
|
|
errdefer buf.deinit();
|
|
const w = encode.ByteWriter.init(&buf);
|
|
try putLenPrefixed(w, table);
|
|
try w.putU32(offset);
|
|
try w.putU32(limit);
|
|
try w.byte(@intCast(orderBy.len));
|
|
for (orderBy) |ob| {
|
|
try w.byte(if (ob.descending) 1 else 0);
|
|
try putLenPrefixed(w, ob.field);
|
|
}
|
|
try w.slice(exprText);
|
|
return buf.toOwnedSlice();
|
|
}
|
|
|
|
pub const TableSelectArgs = struct {
|
|
table: []const u8,
|
|
exprText: []const u8,
|
|
offset: u32 = 0,
|
|
limit: u32 = 0,
|
|
orderBy: []const querymod.OrderBy = &.{},
|
|
};
|
|
|
|
/// `table`, `exprText`, and every `orderBy[i].field` are borrowed slices
|
|
/// into `payload` - the same convention `decodeCollOp` uses. Only the
|
|
/// `orderBy` array itself is owned by the caller (free with
|
|
/// `allocator.free`); never its contents.
|
|
pub fn decodeTableSelect(allocator: Allocator, payload: []const u8) !TableSelectArgs {
|
|
var c = Cursor{ .data = payload };
|
|
const table = try c.takeLenPrefixed();
|
|
const offset = try c.takeU32();
|
|
const limit = try c.takeU32();
|
|
const count = try c.takeByte();
|
|
var list = try std.ArrayList(querymod.OrderBy).initCapacity(allocator, count);
|
|
errdefer list.deinit();
|
|
var i: u8 = 0;
|
|
while (i < count) : (i += 1) {
|
|
const descending = try c.takeByte();
|
|
const field = try c.takeLenPrefixed();
|
|
list.appendAssumeCapacity(.{ .field = field, .descending = descending != 0 });
|
|
}
|
|
const exprText = c.data[c.pos..];
|
|
return .{ .table = table, .exprText = exprText, .offset = offset, .limit = limit, .orderBy = try list.toOwnedSlice() };
|
|
}
|
|
|
|
/// Encodes a `table select` result set as a length-prefixed list of rows,
|
|
/// each row itself encoded with `Row.encode` (the same self-describing
|
|
/// field format a Table stores on disk).
|
|
pub fn encodeRows(allocator: Allocator, rows: []const rowmod.Row) ![]u8 {
|
|
var buf = std.ArrayList(u8).init(allocator);
|
|
errdefer buf.deinit();
|
|
const w = encode.ByteWriter.init(&buf);
|
|
try w.putU32(@intCast(rows.len));
|
|
for (rows) |*r| {
|
|
const encoded = try r.encode(allocator);
|
|
defer allocator.free(encoded);
|
|
try putLenPrefixed(w, encoded);
|
|
}
|
|
return buf.toOwnedSlice();
|
|
}
|
|
|
|
/// Decodes a `table select` result set into independently owned Rows -
|
|
/// callers must free each with `Row.deinit` (see `freeRows` below).
|
|
pub fn decodeRows(allocator: Allocator, payload: []const u8) ![]rowmod.Row {
|
|
var c = Cursor{ .data = payload };
|
|
const count = try c.takeU32();
|
|
var list = try std.ArrayList(rowmod.Row).initCapacity(allocator, count);
|
|
errdefer {
|
|
for (list.items) |*r| r.deinit();
|
|
list.deinit();
|
|
}
|
|
var i: u32 = 0;
|
|
while (i < count) : (i += 1) {
|
|
const raw = try c.takeLenPrefixed();
|
|
const row = rowmod.Row.decode(allocator, raw) catch return error.Malformed;
|
|
list.appendAssumeCapacity(row);
|
|
}
|
|
return list.toOwnedSlice();
|
|
}
|
|
|
|
pub fn freeRows(allocator: Allocator, rows: []rowmod.Row) void {
|
|
for (rows) |*r| r.deinit();
|
|
allocator.free(rows);
|
|
}
|
|
|
|
// --- table join ---
|
|
|
|
/// Which of `join.zig`'s four equi-join variants a `table_join` request
|
|
/// wants - the wire counterpart of the CLI's `table join`
|
|
/// `[--left|--right|--full]` flags (no flag means `.inner`).
|
|
pub const JoinMode = enum(u8) {
|
|
inner = 0,
|
|
left = 1,
|
|
right = 2,
|
|
full = 3,
|
|
|
|
pub fn fromByte(b: u8) !JoinMode {
|
|
return std.meta.intToEnum(JoinMode, b) catch error.UnknownJoinMode;
|
|
}
|
|
};
|
|
|
|
/// Wire request for a remote `table join`: which variant (`mode`), the two
|
|
/// tables and the fields they are joined on, and an optional `expr.zig`
|
|
/// predicate text for each side - the same `leftPred`/`rightPred` filter
|
|
/// `join.innerJoin` and its outer variants already take, evaluated exactly
|
|
/// like `table_select`'s `exprText` (empty selects every row on that side).
|
|
pub fn encodeTableJoin(
|
|
allocator: Allocator,
|
|
mode: JoinMode,
|
|
leftTable: []const u8,
|
|
leftField: []const u8,
|
|
rightTable: []const u8,
|
|
rightField: []const u8,
|
|
offset: u32,
|
|
limit: u32,
|
|
leftExprText: []const u8,
|
|
rightExprText: []const u8,
|
|
) ![]u8 {
|
|
var buf = std.ArrayList(u8).init(allocator);
|
|
errdefer buf.deinit();
|
|
const w = encode.ByteWriter.init(&buf);
|
|
try w.byte(@intFromEnum(mode));
|
|
try putLenPrefixed(w, leftTable);
|
|
try putLenPrefixed(w, leftField);
|
|
try putLenPrefixed(w, rightTable);
|
|
try putLenPrefixed(w, rightField);
|
|
try w.putU32(offset);
|
|
try w.putU32(limit);
|
|
try putLenPrefixed(w, leftExprText);
|
|
try w.slice(rightExprText);
|
|
return buf.toOwnedSlice();
|
|
}
|
|
|
|
pub const TableJoinArgs = struct {
|
|
mode: JoinMode,
|
|
leftTable: []const u8,
|
|
leftField: []const u8,
|
|
rightTable: []const u8,
|
|
rightField: []const u8,
|
|
offset: u32 = 0,
|
|
limit: u32 = 0,
|
|
leftExprText: []const u8,
|
|
rightExprText: []const u8,
|
|
};
|
|
|
|
pub fn decodeTableJoin(payload: []const u8) !TableJoinArgs {
|
|
var c = Cursor{ .data = payload };
|
|
const mode = try JoinMode.fromByte(try c.takeByte());
|
|
const leftTable = try c.takeLenPrefixed();
|
|
const leftField = try c.takeLenPrefixed();
|
|
const rightTable = try c.takeLenPrefixed();
|
|
const rightField = try c.takeLenPrefixed();
|
|
const offset = try c.takeU32();
|
|
const limit = try c.takeU32();
|
|
const leftExprText = try c.takeLenPrefixed();
|
|
const rightExprText = c.data[c.pos..];
|
|
return .{
|
|
.mode = mode,
|
|
.leftTable = leftTable,
|
|
.leftField = leftField,
|
|
.rightTable = rightTable,
|
|
.rightField = rightField,
|
|
.offset = offset,
|
|
.limit = limit,
|
|
.leftExprText = leftExprText,
|
|
.rightExprText = rightExprText,
|
|
};
|
|
}
|
|
|
|
/// One row pair from any of `join.zig`'s four variants, flattened to a
|
|
/// single wire shape: `innerJoin` never leaves either side null,
|
|
/// `leftOuterJoin`/`rightOuterJoin` leave exactly one side nullable, and
|
|
/// `fullOuterJoin` allows either - rather than four near-identical wire
|
|
/// encodings, every variant's result is converted to this one shape before
|
|
/// `encodeJoinedRows`, and the CLI decides what "(nil)" means from which
|
|
/// `--left`/`--right`/`--full` flag it sent, exactly like the local `table
|
|
/// join` printer already does per `JoinMode`.
|
|
pub const JoinPair = struct { left: ?rowmod.Row, right: ?rowmod.Row };
|
|
|
|
fn putOptionalRow(allocator: Allocator, w: encode.ByteWriter, row: ?rowmod.Row) !void {
|
|
if (row) |*r| {
|
|
try w.byte(1);
|
|
const encoded = try r.encode(allocator);
|
|
defer allocator.free(encoded);
|
|
try putLenPrefixed(w, encoded);
|
|
} else {
|
|
try w.byte(0);
|
|
}
|
|
}
|
|
|
|
pub fn encodeJoinedRows(allocator: Allocator, pairs: []const JoinPair) ![]u8 {
|
|
var buf = std.ArrayList(u8).init(allocator);
|
|
errdefer buf.deinit();
|
|
const w = encode.ByteWriter.init(&buf);
|
|
try w.putU32(@intCast(pairs.len));
|
|
for (pairs) |p| {
|
|
try putOptionalRow(allocator, w, p.left);
|
|
try putOptionalRow(allocator, w, p.right);
|
|
}
|
|
return buf.toOwnedSlice();
|
|
}
|
|
|
|
fn takeOptionalRow(allocator: Allocator, c: *Cursor) !?rowmod.Row {
|
|
const present = try c.takeByte();
|
|
if (present == 0) return null;
|
|
const raw = try c.takeLenPrefixed();
|
|
return rowmod.Row.decode(allocator, raw) catch return error.Malformed;
|
|
}
|
|
|
|
/// Decodes a `table_join` result set into independently owned `JoinPair`s -
|
|
/// callers must free each present side with `Row.deinit` (see
|
|
/// `freeJoinPairs` below).
|
|
pub fn decodeJoinedRows(allocator: Allocator, payload: []const u8) ![]JoinPair {
|
|
var c = Cursor{ .data = payload };
|
|
const count = try c.takeU32();
|
|
var list = try std.ArrayList(JoinPair).initCapacity(allocator, count);
|
|
errdefer {
|
|
for (list.items) |*p| {
|
|
if (p.left) |*l| l.deinit();
|
|
if (p.right) |*r| r.deinit();
|
|
}
|
|
list.deinit();
|
|
}
|
|
var i: u32 = 0;
|
|
while (i < count) : (i += 1) {
|
|
const left = try takeOptionalRow(allocator, &c);
|
|
const right = try takeOptionalRow(allocator, &c);
|
|
list.appendAssumeCapacity(.{ .left = left, .right = right });
|
|
}
|
|
return list.toOwnedSlice();
|
|
}
|
|
|
|
pub fn freeJoinPairs(allocator: Allocator, pairs: []JoinPair) void {
|
|
for (pairs) |*p| {
|
|
if (p.left) |*l| l.deinit();
|
|
if (p.right) |*r| r.deinit();
|
|
}
|
|
allocator.free(pairs);
|
|
}
|
|
|
|
// --- table aggregate ---
|
|
|
|
/// Wire request for a remote `table aggregate`: a table name, an optional
|
|
/// GROUP BY field (empty means no grouping - the network counterpart of the
|
|
/// CLI's `*` sentinel for `table aggregate <table> *`, chosen client-side so
|
|
/// the wire itself needs no separate mode byte), the list of
|
|
/// `aggregate.zig` specs to compute, and an `expr.zig` predicate text
|
|
/// filtering rows BEFORE aggregation runs - the same empty-means-every-row
|
|
/// convention `table_select`'s `exprText` already uses.
|
|
/// `offset`/`limit` window the result GROUPS (not the input rows) when
|
|
/// `groupField` is non-empty, the same `join.applyPage`/`Table.selectPage`
|
|
/// convention as `table_join`'s wire format - 0/0 means unlimited. Ignored
|
|
/// server-side when `groupField` is empty (`runGlobal` always produces
|
|
/// exactly one result row, nothing to page).
|
|
/// `orderBy` is a `query.OrderBy` sort key list (possibly empty) applied
|
|
/// server-side to the result GROUPS via `aggregate.sortGroupsBy`, after
|
|
/// HAVING narrows them and before `offset`/`limit` window them - same
|
|
/// convention and wire shape (count byte + descending byte + length-prefixed
|
|
/// field name, per column) as `encodeTableSelect`'s `orderBy`. Each
|
|
/// `orderBy[i].field` must name either `groupField` or one of `specs`'
|
|
/// `specLabel`s (validated server-side by `sortGroupsBy`, not here).
|
|
/// Meaningless (and ignored server-side) for the ungrouped `groupField.len
|
|
/// == 0` path, same as `offset`/`limit` already are - `runGlobal` always
|
|
/// produces exactly one result row, nothing to order.
|
|
///
|
|
/// `havingText` is a second, independent `expr.zig` predicate text
|
|
/// (empty means no HAVING) filtering the result GROUPS AFTER aggregation,
|
|
/// evaluated server-side before `offset`/`limit` window them - the wire
|
|
/// counterpart of the CLI's `having <expr>` suffix. Encoded length-prefixed
|
|
/// (not "rest of payload" like `exprText`) precisely because `exprText`
|
|
/// still needs to occupy that final slot.
|
|
pub fn encodeTableAggregate(allocator: Allocator, table: []const u8, groupField: []const u8, specs: []const aggregatemod.AggSpec, offset: u32, limit: u32, orderBy: []const querymod.OrderBy, havingText: []const u8, exprText: []const u8) ![]u8 {
|
|
var buf = std.ArrayList(u8).init(allocator);
|
|
errdefer buf.deinit();
|
|
const w = encode.ByteWriter.init(&buf);
|
|
try putLenPrefixed(w, table);
|
|
try putLenPrefixed(w, groupField);
|
|
try w.putU32(@intCast(specs.len));
|
|
for (specs) |spec| {
|
|
try w.byte(@intFromEnum(spec.kind));
|
|
try putLenPrefixed(w, spec.field);
|
|
}
|
|
try w.putU32(offset);
|
|
try w.putU32(limit);
|
|
try w.byte(@intCast(orderBy.len));
|
|
for (orderBy) |ob| {
|
|
try w.byte(if (ob.descending) 1 else 0);
|
|
try putLenPrefixed(w, ob.field);
|
|
}
|
|
try putLenPrefixed(w, havingText);
|
|
try w.slice(exprText);
|
|
return buf.toOwnedSlice();
|
|
}
|
|
|
|
pub const TableAggregateArgs = struct {
|
|
table: []const u8,
|
|
groupField: []const u8,
|
|
/// Borrows `field` slices from the decoded payload but owns the slice
|
|
/// itself - caller frees with `allocator.free(args.specs)`, same shape
|
|
/// as `decodeBatch`'s returned list.
|
|
specs: []aggregatemod.AggSpec,
|
|
offset: u32 = 0,
|
|
limit: u32 = 0,
|
|
/// Empty means no ORDER BY - see `encodeTableAggregate`'s doc. Borrows
|
|
/// `field` slices from the decoded payload but owns the slice itself,
|
|
/// same convention as `specs`/`TableSelectArgs.orderBy`.
|
|
orderBy: []const querymod.OrderBy = &.{},
|
|
/// Empty means no HAVING clause - see `encodeTableAggregate`'s doc.
|
|
havingText: []const u8,
|
|
exprText: []const u8,
|
|
};
|
|
|
|
pub fn decodeTableAggregate(allocator: Allocator, payload: []const u8) !TableAggregateArgs {
|
|
var c = Cursor{ .data = payload };
|
|
const table = try c.takeLenPrefixed();
|
|
const groupField = try c.takeLenPrefixed();
|
|
const count = try c.takeU32();
|
|
var specs = try std.ArrayList(aggregatemod.AggSpec).initCapacity(allocator, count);
|
|
errdefer specs.deinit();
|
|
var i: u32 = 0;
|
|
while (i < count) : (i += 1) {
|
|
const kindByte = try c.takeByte();
|
|
const kind = std.meta.intToEnum(aggregatemod.AggKind, kindByte) catch return error.Malformed;
|
|
const field = try c.takeLenPrefixed();
|
|
specs.appendAssumeCapacity(.{ .kind = kind, .field = field });
|
|
}
|
|
const offset = try c.takeU32();
|
|
const limit = try c.takeU32();
|
|
const orderCount = try c.takeByte();
|
|
var orderBy = try std.ArrayList(querymod.OrderBy).initCapacity(allocator, orderCount);
|
|
errdefer orderBy.deinit();
|
|
var j: u8 = 0;
|
|
while (j < orderCount) : (j += 1) {
|
|
const descending = try c.takeByte();
|
|
const field = try c.takeLenPrefixed();
|
|
orderBy.appendAssumeCapacity(.{ .field = field, .descending = descending != 0 });
|
|
}
|
|
const havingText = try c.takeLenPrefixed();
|
|
const exprText = c.data[c.pos..];
|
|
return .{ .table = table, .groupField = groupField, .specs = try specs.toOwnedSlice(), .offset = offset, .limit = limit, .orderBy = try orderBy.toOwnedSlice(), .havingText = havingText, .exprText = exprText };
|
|
}
|
|
|
|
fn putEncodedValue(allocator: Allocator, w: encode.ByteWriter, value: rowmod.Value) !void {
|
|
const encoded = try rowmod.encodeValue(allocator, value);
|
|
defer allocator.free(encoded);
|
|
try putLenPrefixed(w, encoded);
|
|
}
|
|
|
|
fn takeDecodedValue(allocator: Allocator, c: *Cursor) !rowmod.Value {
|
|
const raw = try c.takeLenPrefixed();
|
|
return rowmod.decodeValue(allocator, raw) catch return error.Malformed;
|
|
}
|
|
|
|
/// Encodes a `runGlobal` (no `GROUP BY`) result: exactly `specs.len` values,
|
|
/// same order as the request's specs.
|
|
pub fn encodeAggGlobalResult(allocator: Allocator, values: []const rowmod.Value) ![]u8 {
|
|
var buf = std.ArrayList(u8).init(allocator);
|
|
errdefer buf.deinit();
|
|
const w = encode.ByteWriter.init(&buf);
|
|
try w.putU32(@intCast(values.len));
|
|
for (values) |v| try putEncodedValue(allocator, w, v);
|
|
return buf.toOwnedSlice();
|
|
}
|
|
|
|
/// Decodes a `runGlobal` result into independently owned `Value`s - callers
|
|
/// must free with `freeAggValues`.
|
|
pub fn decodeAggGlobalResult(allocator: Allocator, payload: []const u8) ![]rowmod.Value {
|
|
var c = Cursor{ .data = payload };
|
|
const count = try c.takeU32();
|
|
var list = try std.ArrayList(rowmod.Value).initCapacity(allocator, count);
|
|
errdefer {
|
|
for (list.items) |v| v.free(allocator);
|
|
list.deinit();
|
|
}
|
|
var i: u32 = 0;
|
|
while (i < count) : (i += 1) list.appendAssumeCapacity(try takeDecodedValue(allocator, &c));
|
|
return list.toOwnedSlice();
|
|
}
|
|
|
|
pub fn freeAggValues(allocator: Allocator, values: []rowmod.Value) void {
|
|
for (values) |v| v.free(allocator);
|
|
allocator.free(values);
|
|
}
|
|
|
|
/// Encodes a grouped `run` result: one entry per `GroupRow` (the group-key
|
|
/// value, then exactly `specs.len` result values), in the same first-seen
|
|
/// order `GroupBy.finish()` produced them.
|
|
pub fn encodeAggGroupRows(allocator: Allocator, groups: []const aggregatemod.GroupRow) ![]u8 {
|
|
var buf = std.ArrayList(u8).init(allocator);
|
|
errdefer buf.deinit();
|
|
const w = encode.ByteWriter.init(&buf);
|
|
try w.putU32(@intCast(groups.len));
|
|
for (groups) |g| {
|
|
try putEncodedValue(allocator, w, g.group);
|
|
try w.putU32(@intCast(g.values.len));
|
|
for (g.values) |v| try putEncodedValue(allocator, w, v);
|
|
}
|
|
return buf.toOwnedSlice();
|
|
}
|
|
|