feat: add decodeValue to row

This commit is contained in:
dualfroz committed 2026-09-10 20:32:26 +02:00
1 parent 875d8fe62f
commit 65e5da36e5
1 file changed
+170
+170
View File
@@ -315,3 +315,173 @@ pub fn encodeValue(allocator: Allocator, value: Value) ![]u8 {
return out.toOwnedSlice();
}
pub fn decodeValue(allocator: Allocator, data: []const u8) !Value {
var pos: usize = 0;
if (pos >= data.len) return error.Malformed;
const tag: ValueTag = std.meta.intToEnum(ValueTag, data[pos]) catch return error.Malformed;
pos += 1;
return switch (tag) {
.none => .{ .none = {} },
.boolean => blk: {
if (pos >= data.len) return error.Malformed;
break :blk .{ .boolean = data[pos] != 0 };
},
.int => .{ .int = @bitCast(try readU64(data, &pos)) },
.float => .{ .float = @bitCast(try readU64(data, &pos)) },
.text => blk: {
const len = try readU32(data, &pos);
if (pos + len > data.len) return error.Malformed;
break :blk .{ .text = try allocator.dupe(u8, data[pos .. pos + len]) };
},
.bytes => blk: {
const len = try readU32(data, &pos);
if (pos + len > data.len) return error.Malformed;
break :blk .{ .bytes = try allocator.dupe(u8, data[pos .. pos + len]) };
},
};
}
test "row set get overwrite and free" {
const allocator = std.testing.allocator;
var row = Row.init(allocator);
defer row.deinit();
try row.set("name", .{ .text = "ada" });
try row.set("age", .{ .int = 30 });
try row.set("name", .{ .text = "ada lovelace" });
try std.testing.expectEqual(@as(usize, 2), row.count());
try std.testing.expectEqualStrings("ada lovelace", row.get("name").?.text);
try std.testing.expectEqual(@as(i64, 30), row.get("age").?.int);
try std.testing.expect(row.get("missing") == null);
}
test "row encode decode round trip all types" {
const allocator = std.testing.allocator;
var row = Row.init(allocator);
defer row.deinit();
try row.set("id", .{ .int = -42 });
try row.set("score", .{ .float = 3.5 });
try row.set("active", .{ .boolean = true });
try row.set("bio", .{ .text = "hello world" });
try row.set("blob", .{ .bytes = &.{ 0, 1, 2, 255 } });
try row.set("nothing", .{ .none = {} });
const encoded = try row.encode(allocator);
defer allocator.free(encoded);
var decoded = try Row.decode(allocator, encoded);
defer decoded.deinit();
try std.testing.expectEqual(@as(i64, -42), decoded.get("id").?.int);
try std.testing.expectEqual(@as(f64, 3.5), decoded.get("score").?.float);
try std.testing.expectEqual(true, decoded.get("active").?.boolean);
try std.testing.expectEqualStrings("hello world", decoded.get("bio").?.text);
try std.testing.expectEqualSlices(u8, &.{ 0, 1, 2, 255 }, decoded.get("blob").?.bytes);
try std.testing.expectEqual(ValueTag.none, @as(ValueTag, decoded.get("nothing").?));
}
test "row decode rejects malformed data" {
const allocator = std.testing.allocator;
try std.testing.expectError(error.Malformed, Row.decode(allocator, &.{ 1, 0, 0, 0 }));
try std.testing.expectError(error.Malformed, Row.decode(allocator, &.{ 1, 0, 0, 0, 5, 0, 'a' }));
}
test "compare orders by type tag then payload" {
try std.testing.expectEqual(std.math.Order.lt, compare(.{ .none = {} }, .{ .boolean = false }));
try std.testing.expectEqual(std.math.Order.lt, compare(.{ .int = 5 }, .{ .float = 0.0 }));
try std.testing.expectEqual(std.math.Order.lt, compare(.{ .int = -5 }, .{ .int = 3 }));
try std.testing.expectEqual(std.math.Order.gt, compare(.{ .text = "b" }, .{ .text = "a" }));
try std.testing.expect(Value.eql(.{ .text = "x" }, .{ .text = "x" }));
}
test "appendOrdered preserves int order across sign boundary" {
const allocator = std.testing.allocator;
const values = [_]i64{ std.math.minInt(i64), -1000, -1, 0, 1, 1000, std.math.maxInt(i64) };
var prev: ?[]u8 = null;
defer if (prev) |p| allocator.free(p);
for (values) |v| {
const encoded = try encodeOrdered(allocator, .{ .int = v });
if (prev) |p| {
try std.testing.expect(std.mem.order(u8, p, encoded) == .lt);
allocator.free(p);
}
prev = encoded;
}
}
test "appendOrdered preserves float order across sign and magnitude" {
const allocator = std.testing.allocator;
const values = [_]f64{ -100.0, -1.5, -0.001, 0.0, 0.001, 1.5, 100.0 };
var prev: ?[]u8 = null;
defer if (prev) |p| allocator.free(p);
for (values) |v| {
const encoded = try encodeOrdered(allocator, .{ .float = v });
if (prev) |p| {
try std.testing.expect(std.mem.order(u8, p, encoded) == .lt);
allocator.free(p);
}
prev = encoded;
}
}
test "appendOrdered escapes embedded zero bytes unambiguously" {
const allocator = std.testing.allocator;
const a = try encodeOrdered(allocator, .{ .text = "ab" });
defer allocator.free(a);
const b = try encodeOrdered(allocator, .{ .text = "ab\x00c" });
defer allocator.free(b);
const c = try encodeOrdered(allocator, .{ .text = "ab" ++ "c"[0..0] });
defer allocator.free(c);
// "ab" must sort before "ab\x00c" (shorter, unescaped terminator wins),
// and the escaped embedded zero must not corrupt ordering.
try std.testing.expect(std.mem.order(u8, a, b) == .lt);
try std.testing.expect(std.mem.eql(u8, a, c));
}
test "encodeValue decodeValue round trip" {
const allocator = std.testing.allocator;
const original = Value{ .text = "primary-key-42" };
const encoded = try encodeValue(allocator, original);
defer allocator.free(encoded);
const decoded = try decodeValue(allocator, encoded);
defer decoded.free(allocator);
try std.testing.expectEqualStrings("primary-key-42", decoded.text);
const intEncoded = try encodeValue(allocator, .{ .int = -7 });
defer allocator.free(intEncoded);
const intDecoded = try decodeValue(allocator, intEncoded);
try std.testing.expectEqual(@as(i64, -7), intDecoded.int);
}
test "appendOrdered text ordering matches string ordering" {
const allocator = std.testing.allocator;
const words = [_][]const u8{ "apple", "banana", "banjo", "zebra" };
var prev: ?[]u8 = null;
defer if (prev) |p| allocator.free(p);
for (words) |w| {
const encoded = try encodeOrdered(allocator, .{ .text = w });
if (prev) |p| {
try std.testing.expect(std.mem.order(u8, p, encoded) == .lt);
allocator.free(p);
}
prev = encoded;
}
}
test "parseCli honors type prefixes and defaults to text" {
try std.testing.expectEqual(@as(i64, 42), (try Value.parseCli("i:42")).int);
try std.testing.expectEqual(@as(i64, -3), (try Value.parseCli("i:-3")).int);
try std.testing.expectEqual(@as(f64, 1.5), (try Value.parseCli("f:1.5")).float);
try std.testing.expectEqual(true, (try Value.parseCli("b:true")).boolean);
try std.testing.expectEqual(false, (try Value.parseCli("b:false")).boolean);
try std.testing.expectEqualStrings("hi", (try Value.parseCli("s:hi")).text);
try std.testing.expectEqualStrings("plain", (try Value.parseCli("plain")).text);
}
test "parseCli rejects malformed prefixed values" {
try std.testing.expectError(error.InvalidValue, Value.parseCli("i:notanumber"));
try std.testing.expectError(error.InvalidValue, Value.parseCli("f:notafloat"));
try std.testing.expectError(error.InvalidValue, Value.parseCli("b:maybe"));
}