feat: add ValueTag and Value

This commit is contained in:
dualfroz committed 2026-09-10 18:40:14 +02:00
1 parent b9873660ac
commit a4eaa8e896
1 file changed
+160
+160
View File
@@ -0,0 +1,160 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
/// Dynamically typed value stored inside a row field, an index key, or a
/// primary key. Kept small and copyable by tag; owned byte payloads (text,
/// bytes) are heap-allocated and must be freed by whoever owns the Row/Value.
pub const ValueTag = enum(u8) {
none = 0,
boolean = 1,
int = 2,
float = 3,
text = 4,
bytes = 5,
};
pub const Value = union(ValueTag) {
none: void,
boolean: bool,
int: i64,
float: f64,
text: []const u8,
bytes: []const u8,
pub fn dupe(self: Value, allocator: Allocator) !Value {
return switch (self) {
.text => |s| .{ .text = try allocator.dupe(u8, s) },
.bytes => |s| .{ .bytes = try allocator.dupe(u8, s) },
else => self,
};
}
pub fn free(self: Value, allocator: Allocator) void {
switch (self) {
.text => |s| allocator.free(s),
.bytes => |s| allocator.free(s),
else => {},
}
}
pub fn eql(a: Value, b: Value) bool {
return compare(a, b) == .eq;
}
/// Parses a single CLI/query-language token into a typed Value, using the
/// same `i:`/`f:`/`b:`/`s:` type-prefix convention throughout the CLI
/// (unprefixed tokens default to text). The returned `.text`/`.bytes`
/// payload (when present) is a slice into `text` - callers that need it
/// to outlive `text` must `dupe` the result.
pub fn parseCli(text: []const u8) error{InvalidValue}!Value {
if (std.mem.startsWith(u8, text, "i:")) {
const n = std.fmt.parseInt(i64, text[2..], 10) catch return error.InvalidValue;
return .{ .int = n };
} else if (std.mem.startsWith(u8, text, "f:")) {
const f = std.fmt.parseFloat(f64, text[2..]) catch return error.InvalidValue;
return .{ .float = f };
} else if (std.mem.startsWith(u8, text, "b:")) {
if (std.mem.eql(u8, text[2..], "true")) return .{ .boolean = true };
if (std.mem.eql(u8, text[2..], "false")) return .{ .boolean = false };
return error.InvalidValue;
} else if (std.mem.startsWith(u8, text, "s:")) {
return .{ .text = text[2..] };
} else {
return .{ .text = text };
}
}
};
/// Total order across all values: first by type tag, then by payload. This
/// keeps a well-defined ordering even when a field holds mixed types across
/// rows, which the ordered byte encoding below must mirror exactly.
pub fn compare(a: Value, b: Value) std.math.Order {
const ta = @intFromEnum(@as(ValueTag, a));
const tb = @intFromEnum(@as(ValueTag, b));
if (ta != tb) return std.math.order(ta, tb);
return switch (a) {
.none => .eq,
.boolean => |av| std.math.order(@intFromBool(av), @intFromBool(b.boolean)),
.int => |av| std.math.order(av, b.int),
.float => |av| std.math.order(av, b.float),
.text => |av| std.mem.order(u8, av, b.text),
.bytes => |av| std.mem.order(u8, av, b.bytes),
};
}
pub const Field = struct {
name: []const u8,
value: Value,
};
/// Self-describing, schema-less row: an ordered list of named fields. Rows
/// are the unit stored under a table's primary-key row keys and are what
/// query.zig hands back from scans and index lookups.
pub const Row = struct {
allocator: Allocator,
fields: std.ArrayList(Field),
pub fn init(allocator: Allocator) Row {
return .{ .allocator = allocator, .fields = std.ArrayList(Field).init(allocator) };
}
pub fn deinit(self: *Row) void {
for (self.fields.items) |f| {
self.allocator.free(f.name);
f.value.free(self.allocator);
}
self.fields.deinit();
}
/// Sets a field, replacing any existing field with the same name. Copies
/// both the name and any owned payload of `value` into the row.
pub fn set(self: *Row, name: []const u8, value: Value) !void {
const owned = try value.dupe(self.allocator);
errdefer owned.free(self.allocator);
for (self.fields.items) |*f| {
if (std.mem.eql(u8, f.name, name)) {
f.value.free(self.allocator);
f.value = owned;
return;
}
}
const nameCopy = try self.allocator.dupe(u8, name);
try self.fields.append(.{ .name = nameCopy, .value = owned });
}
pub fn get(self: *const Row, name: []const u8) ?Value {
for (self.fields.items) |f| {
if (std.mem.eql(u8, f.name, name)) return f.value;
}
return null;
}
pub fn count(self: *const Row) usize {
return self.fields.items.len;
}
/// Serializes the row to a self-contained byte blob suitable as a Db
/// value: [u32 fieldCount] { [u16 nameLen][name][u8 tag][payload] }*.
pub fn encode(self: *const Row, allocator: Allocator) ![]u8 {
var out = std.ArrayList(u8).init(allocator);
errdefer out.deinit();
try putU32(&out, @intCast(self.fields.items.len));
for (self.fields.items) |f| {
try putU16(&out, @intCast(f.name.len));
try out.appendSlice(f.name);
try out.append(@intFromEnum(@as(ValueTag, f.value)));
switch (f.value) {
.none => {},
.boolean => |v| try out.append(if (v) 1 else 0),
.int => |v| try putU64(&out, @bitCast(v)),
.float => |v| try putU64(&out, @bitCast(v)),
.text, .bytes => |s| {
try putU32(&out, @intCast(s.len));
try out.appendSlice(s);
},
}
}
return out.toOwnedSlice();
}