diff --git a/src/server.zig b/src/server.zig index 775f325..139bf97 100644 --- a/src/server.zig +++ b/src/server.zig @@ -3125,3 +3125,162 @@ test "server table_delete rejects a malformed expr with bad_request, not a crash 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 client_mod = @import("client.zig"); + var client = try client_mod.Client.connect(allocator, server.boundAddress()); + defer client.close(); + + const err = client.tableDelete("users", "city bogus s:warsaw"); + try std.testing.expectError(client_mod.ClientError.ServerBadRequest, err); + + // The connection survives a bad_request and can still serve a good + // request right after - same expectation `table_update`'s malformed + // test above holds for its own opcode. + const deleted = try client.tableDelete("users", ""); + try std.testing.expectEqual(@as(u64, 1), deleted); +} + +// `handleTableDelete` calls `Table.deleteWhere` directly (see its own +// source), the exact function `Table.delete`'s ON DELETE RESTRICT check +// (query.zig, backed by `ReferencedBySet`) is wired into - so this was +// never a separate opcode to teach about FOREIGN KEY, only a round trip to +// prove the wire path actually reaches it, the same "real dispatch, not +// just the in-process unit test" shape the CHECK/FK/UNIQUE `txn_op` tests +// elsewhere in this file already are. +test "server table_delete rejects a FOREIGN KEY RESTRICT violation over the wire and leaves the row in place" { + 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 customers = try querymod.Table.open(&db, allocator, "customers"); + defer customers.deinit(); + try customers.put(.{ .int = 1 }, &.{}); + + var orders = try querymod.Table.open(&db, allocator, "orders"); + defer orders.deinit(); + try orders.createForeignKey("customer_id", "customers", querymod.pkField, .restrict); + try orders.put(.{ .int = 100 }, &.{.{ .name = "customer_id", .value = .{ .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 client_mod = @import("client.zig"); + var client = try client_mod.Client.connect(allocator, server.boundAddress()); + defer client.close(); + + const err = client.tableDelete("customers", ""); + try std.testing.expectError(client_mod.ClientError.ServerInternal, err); + + var customers = try querymod.Table.open(&db, allocator, "customers"); + defer customers.deinit(); + var stillThere = (try customers.get(.{ .int = 1 })).?; + defer stillThere.deinit(); + + // Once the referencing order is gone, the same request succeeds. + _ = try client.tableDelete("orders", ""); + const deleted = try client.tableDelete("customers", ""); + try std.testing.expectEqual(@as(u64, 1), deleted); +} + +test "server rejects a malformed table_delete frame (bad table name) 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(); + + 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 length prefix (0xFFFFFFFF) claiming far more table-name bytes than + // the payload actually carries - `decodeTableDelete`'s own malformed-frame + // check must catch 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, 0xFFFFFFFF, .little); + try payload.appendSlice(&lenBuf); + + try protocol.writeFrame(stream.writer(), @intFromEnum(protocol.Op.table_delete), payload.items); + var frame = try protocol.readFrame(allocator, stream.reader()); + defer frame.deinit(); + try std.testing.expectEqual(@intFromEnum(protocol.Status.bad_request), frame.tag); + + // The connection must still be usable afterward. + try protocol.writeFrame(stream.writer(), @intFromEnum(protocol.Op.ping), ""); + var pingFrame = try protocol.readFrame(allocator, stream.reader()); + defer pingFrame.deinit(); + try std.testing.expectEqual(@intFromEnum(protocol.Status.ok), pingFrame.tag); +} + +test "server table_export renders a real table to csv and json over TCP, matching the local dump.zig output" { + 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 }, &.{ .{ .name = "city", .value = .{ .text = "warsaw" } }, .{ .name = "age", .value = .{ .int = 30 } } }); + try table.put(.{ .int = 2 }, &.{.{ .name = "city", .value = .{ .text = "krakow" } }}); + } + + 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 csvResult = try client.tableExport(allocator, "people", .csv); + defer csvResult.deinit(allocator); + try std.testing.expectEqual(@as(u64, 2), csvResult.rowCount); + + var localCsv = std.ArrayList(u8).init(allocator); + defer localCsv.deinit(); + { + var table = try querymod.Table.open(&db, allocator, "people"); + defer table.deinit(); + const n = try dumpmod.exportCsvWriter(allocator, &table, localCsv.writer()); + try std.testing.expectEqual(@as(usize, 2), n); + } + try std.testing.expectEqualStrings(localCsv.items, csvResult.content); + + var jsonResult = try client.tableExport(allocator, "people", .json); + defer jsonResult.deinit(allocator); + try std.testing.expectEqual(@as(u64, 2), jsonResult.rowCount); + // Every row's primary key shows up somewhere in the rendered JSON - + // enough to prove the server actually rendered real content instead + // of an empty/placeholder body, without re-parsing JSON here too. + try std.testing.expect(std.mem.indexOf(u8, jsonResult.content, "warsaw") != null); + try std.testing.expect(std.mem.indexOf(u8, jsonResult.content, "krakow") != null); +} +