diff --git a/src/row.zig b/src/row.zig index a4c6f03..eb4b449 100644 --- a/src/row.zig +++ b/src/row.zig @@ -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")); +}