diff --git a/src/server.zig b/src/server.zig index 139bf97..05618f3 100644 --- a/src/server.zig +++ b/src/server.zig @@ -3284,3 +3284,160 @@ test "server table_export renders a real table to csv and json over TCP, matchin try std.testing.expect(std.mem.indexOf(u8, jsonResult.content, "krakow") != null); } +test "server table_import loads csv and json content sent over TCP into a real table" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + const path = try tmp.dir.realpathAlloc(allocator, "."); + defer allocator.free(path); + + var db = try testDb(allocator, path); + defer db.close(); + + const address = try std.net.Address.parseIp("127.0.0.1", 0); + var server = try Server.init(allocator, &db, address); + try server.start(); + defer server.stop(); + + const client_mod = @import("client.zig"); + var client = try client_mod.Client.connect(allocator, server.boundAddress()); + defer client.close(); + + const csvImported = try client.tableImport("from_csv", .csv, "__pk,city\ni:1,s:warsaw\ni:2,s:krakow\n"); + try std.testing.expectEqual(@as(u64, 2), csvImported); + + const jsonImported = try client.tableImport("from_json", .json, "[{\"__pk\": \"i:1\", \"city\": \"s:gdansk\"}]"); + try std.testing.expectEqual(@as(u64, 1), jsonImported); + + const csvRows = try client.tableSelect(allocator, "from_csv", "", 0, 0, &.{}); + defer protocol.freeRows(allocator, csvRows); + try std.testing.expectEqual(@as(usize, 2), csvRows.len); + + const jsonRows = try client.tableSelect(allocator, "from_json", "city eq s:gdansk", 0, 0, &.{}); + defer protocol.freeRows(allocator, jsonRows); + try std.testing.expectEqual(@as(usize, 1), jsonRows.len); +} + +test "server table_export then table_import round trips a real table over two TCP round trips" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + const path = try tmp.dir.realpathAlloc(allocator, "."); + defer allocator.free(path); + + var db = try testDb(allocator, path); + defer db.close(); + + { + var table = try querymod.Table.open(&db, allocator, "source"); + defer table.deinit(); + try table.put(.{ .int = 1 }, &.{ .{ .name = "city", .value = .{ .text = "warsaw" } }, .{ .name = "score", .value = .{ .float = 4.5 } } }); + try table.put(.{ .int = 2 }, &.{.{ .name = "city", .value = .{ .text = "lodz" } }}); + try table.put(.{ .int = 3 }, &.{}); + } + + const address = try std.net.Address.parseIp("127.0.0.1", 0); + var server = try Server.init(allocator, &db, address); + try server.start(); + defer server.stop(); + + const client_mod = @import("client.zig"); + var client = try client_mod.Client.connect(allocator, server.boundAddress()); + defer client.close(); + + var exported = try client.tableExport(allocator, "source", .json); + defer exported.deinit(allocator); + try std.testing.expectEqual(@as(u64, 3), exported.rowCount); + + const imported = try client.tableImport("dest", .json, exported.content); + try std.testing.expectEqual(@as(u64, 3), imported); + + const warsawRows = try client.tableSelect(allocator, "dest", "city eq s:warsaw", 0, 0, &.{}); + defer protocol.freeRows(allocator, warsawRows); + try std.testing.expectEqual(@as(usize, 1), warsawRows.len); + try std.testing.expectEqual(@as(f64, 4.5), warsawRows[0].get("score").?.float); + + const untouched = try client.tableSelect(allocator, "dest", "city eq s:lodz", 0, 0, &.{}); + defer protocol.freeRows(allocator, untouched); + try std.testing.expectEqual(@as(usize, 1), untouched.len); + + const allRows = try client.tableSelect(allocator, "dest", "", 0, 0, &.{}); + defer protocol.freeRows(allocator, allRows); + try std.testing.expectEqual(@as(usize, 3), allRows.len); +} + +test "server table_export succeeds on a never-written table (schemaless tables have no declare step), table_import rejects malformed content without crashing" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + const path = try tmp.dir.realpathAlloc(allocator, "."); + defer allocator.free(path); + + var db = try testDb(allocator, path); + defer db.close(); + + const address = try std.net.Address.parseIp("127.0.0.1", 0); + var server = try Server.init(allocator, &db, address); + try server.start(); + defer server.stop(); + + const client_mod = @import("client.zig"); + var client = try client_mod.Client.connect(allocator, server.boundAddress()); + defer client.close(); + + // `table` is just a keyspace prefix, never declared ahead of time (see + // `query.Table.open`), so exporting one nothing has ever put a row into + // is a valid zero-row export, not an error. + var exported = try client.tableExport(allocator, "nosuch", .csv); + defer exported.deinit(allocator); + try std.testing.expectEqual(@as(u64, 0), exported.rowCount); + + const badImport = client.tableImport("nosuch", .json, "not json"); + try std.testing.expectError(client_mod.ClientError.ServerInternal, badImport); + + // The connection survives an internal error and can still serve a + // good request right after. + const rowCount = try client.tableImport("nosuch", .json, "[]"); + try std.testing.expectEqual(@as(u64, 0), rowCount); +} + +test "server rejects a malformed table_export frame (bad format byte) with bad_request without crashing" { + const allocator = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + const path = try tmp.dir.realpathAlloc(allocator, "."); + defer allocator.free(path); + + var db = try testDb(allocator, path); + defer db.close(); + + { + var table = try querymod.Table.open(&db, allocator, "people"); + defer table.deinit(); + try table.put(.{ .int = 1 }, &.{}); + } + + const address = try std.net.Address.parseIp("127.0.0.1", 0); + var server = try Server.init(allocator, &db, address); + try server.start(); + defer server.stop(); + + const stream = try std.net.tcpConnectToAddress(server.boundAddress()); + defer stream.close(); + + // A well-formed table name ("people") followed by a format byte + // outside the DumpFormat enum (99) - `decodeTableExport` itself must + // reject this before `Table.open` is ever reached. + var payload = std.ArrayList(u8).init(allocator); + defer payload.deinit(); + var lenBuf: [4]u8 = undefined; + std.mem.writeInt(u32, &lenBuf, 6, .little); + try payload.appendSlice(&lenBuf); + try payload.appendSlice("people"); + try payload.append(99); + + try protocol.writeFrame(stream.writer(), @intFromEnum(protocol.Op.table_export), payload.items); + var frame = try protocol.readFrame(allocator, stream.reader()); + defer frame.deinit(); + try std.testing.expectEqual(@intFromEnum(protocol.Status.bad_request), frame.tag); +