feat: add importJsonContent and importJson to dump
This commit is contained in:
1 parent
a3ac6b0c5d
commit
1171d7867e
1 file changed
+157
+157
@@ -320,3 +320,160 @@ pub fn exportJson(allocator: Allocator, table: *Table, path: []const u8) !usize
|
|||||||
return n;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
Reference in new issue
Block a user