From a4eaa8e89662e11730973b0ab736a0a5b7a3e53e Mon Sep 17 00:00:00 2001 From: DualFroz Date: Thu, 10 Sep 2026 18:40:14 +0200 Subject: [PATCH] feat: add ValueTag and Value --- src/row.zig | 160 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 src/row.zig diff --git a/src/row.zig b/src/row.zig new file mode 100644 index 0000000..6717801 --- /dev/null +++ b/src/row.zig @@ -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(); + } +