feat: add ValueTag and Value
This commit is contained in:
1 parent
b9873660ac
commit
a4eaa8e896
1 file changed
+160
+160
@@ -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();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user