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