diff --git a/src/dump.zig b/src/dump.zig index 6629d04..c0d9b62 100644 --- a/src/dump.zig +++ b/src/dump.zig @@ -320,3 +320,160 @@ pub fn exportJson(allocator: Allocator, table: *Table, path: []const u8) !usize return n; } +pub fn importJsonContent(allocator: Allocator, table: *Table, content: []const u8) !usize { + var parsed = std.json.parseFromSlice(std.json.Value, allocator, content, .{}) catch return error.MalformedJson; + defer parsed.deinit(); + + const array = switch (parsed.value) { + .array => |a| a, + else => return error.MalformedJson, + }; + + var count: usize = 0; + for (array.items) |entry| { + const object = switch (entry) { + .object => |o| o, + else => return error.MalformedJson, + }; + + var pk: ?Value = null; + defer if (pk) |p| p.free(allocator); + var fields = std.ArrayList(Field).init(allocator); + defer { + for (fields.items) |f| f.value.free(allocator); + fields.deinit(); + } + + for (object.keys(), object.values()) |name, jv| { + const cell = switch (jv) { + .string => |s| s, + else => return error.MalformedJson, + }; + const value = try decodeCell(allocator, cell); + if (std.mem.eql(u8, name, pkField)) { + pk = value; + } else { + try fields.append(.{ .name = name, .value = value }); + } + } + const pkValue = pk orelse return error.MissingPrimaryKey; + try table.put(pkValue, fields.items); + count += 1; + } + return count; +} + +pub fn importJson(allocator: Allocator, table: *Table, path: []const u8) !usize { + const content = try std.fs.cwd().readFileAlloc(allocator, path, 1 << 30); + defer allocator.free(content); + return importJsonContent(allocator, table, content); +} + +// --------------------------------------------------------------- tests ---- + +const testing = std.testing; + +test "encodeCell decodeCell round trip every value tag" { + const allocator = testing.allocator; + const values = [_]Value{ + .{ .none = {} }, + .{ .boolean = true }, + .{ .boolean = false }, + .{ .int = -42 }, + .{ .int = 0 }, + .{ .float = 3.5 }, + .{ .float = -0.001 }, + .{ .text = "hello, \"world\"\n" }, + .{ .text = "" }, + .{ .bytes = &.{ 0, 1, 2, 255 } }, + }; + for (values) |v| { + const cell = try encodeCell(allocator, v); + defer allocator.free(cell); + const decoded = try decodeCell(allocator, cell); + defer decoded.free(allocator); + try testing.expectEqual(@as(rowmod.ValueTag, v), @as(rowmod.ValueTag, decoded)); + try testing.expect(Value.eql(v, decoded)); + } +} + +test "decodeCell rejects malformed cells" { + const allocator = testing.allocator; + try testing.expectError(error.InvalidCell, decodeCell(allocator, "")); + try testing.expectError(error.InvalidCell, decodeCell(allocator, "x")); + try testing.expectError(error.InvalidCell, decodeCell(allocator, "q:1")); + try testing.expectError(error.InvalidCell, decodeCell(allocator, "i:notanumber")); + try testing.expectError(error.InvalidCell, decodeCell(allocator, "b:maybe")); + try testing.expectError(error.InvalidCell, decodeCell(allocator, "x:abc")); + try testing.expectError(error.InvalidCell, decodeCell(allocator, "x:zz")); +} + +fn openTemp(tmp: *std.testing.TmpDir, allocator: Allocator) !Db { + const dbPath = try tmp.dir.realpathAlloc(allocator, "."); + defer allocator.free(dbPath); + return Db.open(allocator, dbPath, .{}); +} + +fn seedTable(table: *Table) !void { + try table.put(.{ .int = 1 }, &.{ + .{ .name = "city", .value = .{ .text = "warsaw" } }, + .{ .name = "age", .value = .{ .int = 30 } }, + .{ .name = "active", .value = .{ .boolean = true } }, + }); + try table.put(.{ .int = 2 }, &.{ + .{ .name = "city", .value = .{ .text = "krakow, poland \"old town\"\nsecond line" } }, + .{ .name = "score", .value = .{ .float = -12.5 } }, + }); + try table.put(.{ .int = 3 }, &.{}); +} + +fn expectRoundTripped(table: *Table) !void { + var r1 = (try table.get(.{ .int = 1 })).?; + defer r1.deinit(); + try testing.expectEqualStrings("warsaw", r1.get("city").?.text); + try testing.expectEqual(@as(i64, 30), r1.get("age").?.int); + try testing.expectEqual(true, r1.get("active").?.boolean); + + var r2 = (try table.get(.{ .int = 2 })).?; + defer r2.deinit(); + try testing.expectEqualStrings("krakow, poland \"old town\"\nsecond line", r2.get("city").?.text); + try testing.expectEqual(@as(f64, -12.5), r2.get("score").?.float); + try testing.expect(r2.get("age") == null); + + // Row 3 was put with zero extra fields, so its only field is the + // primary key itself (`put` always stores that under `pkField`). + var r3 = (try table.get(.{ .int = 3 })).?; + defer r3.deinit(); + try testing.expectEqual(@as(usize, 1), r3.count()); + try testing.expectEqual(@as(i64, 3), r3.get(pkField).?.int); +} + +test "csv export then import round trips every row and field exactly" { + const allocator = testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + var db = try openTemp(&tmp, allocator); + defer db.close(); + + var table = try Table.open(&db, allocator, "people"); + defer table.deinit(); + try seedTable(&table); + + var buf = std.ArrayList(u8).init(allocator); + defer buf.deinit(); + const exported = try exportCsvWriter(allocator, &table, buf.writer()); + try testing.expectEqual(@as(usize, 3), exported); + + var dstTmp = std.testing.tmpDir(.{}); + defer dstTmp.cleanup(); + var dstDb = try openTemp(&dstTmp, allocator); + defer dstDb.close(); + var dstTable = try Table.open(&dstDb, allocator, "people"); + defer dstTable.deinit(); + + const imported = try importCsvContent(allocator, &dstTable, buf.items); + try testing.expectEqual(@as(usize, 3), imported); + + try expectRoundTripped(&dstTable); +} +