diff --git a/src/join.zig b/src/join.zig index 559de60..99fc00f 100644 --- a/src/join.zig +++ b/src/join.zig @@ -464,3 +464,161 @@ test "innerJoin: unindexed hash join matches on equal field value" { try testing.expectEqual(@as(usize, 3), joined.len); + var aliceItems = std.StringHashMap(void).init(testing.allocator); + defer aliceItems.deinit(); + var seenBob = false; + for (joined) |pair| { + const name = pair.left.get("name").?.text; + const item = pair.right.get("item").?.text; + if (std.mem.eql(u8, name, "alice")) { + try aliceItems.put(item, {}); + } else if (std.mem.eql(u8, name, "bob")) { + seenBob = true; + try testing.expectEqualStrings("gizmo", item); + } else { + try testing.expect(false); // unreachable: only alice/bob should ever match + } + } + try testing.expect(aliceItems.contains("widget")); + try testing.expect(aliceItems.contains("gadget")); + try testing.expect(seenBob); +} + +test "innerJoin: index nested-loop join produces identical results to hash join" { + var tmp = testing.tmpDir(.{}); + defer tmp.cleanup(); + var db = try openTestDb(&tmp); + defer db.close(); + + var customers = try Table.open(&db, testing.allocator, "customers"); + defer customers.deinit(); + try customers.put(.{ .int = 1 }, &.{.{ .name = "name", .value = .{ .text = "alice" } }}); + try customers.put(.{ .int = 2 }, &.{.{ .name = "name", .value = .{ .text = "bob" } }}); + + var orders = try Table.open(&db, testing.allocator, "orders"); + defer orders.deinit(); + try orders.createIndex("customer_id"); + try orders.put(.{ .int = 100 }, &.{.{ .name = "customer_id", .value = .{ .int = 1 } }}); + try orders.put(.{ .int = 101 }, &.{.{ .name = "customer_id", .value = .{ .int = 1 } }}); + try orders.put(.{ .int = 102 }, &.{.{ .name = "customer_id", .value = .{ .int = 2 } }}); + + try testing.expect(orders.hasIndex("customer_id")); + + const joined = try innerJoin(testing.allocator, &customers, "__pk", null, &orders, "customer_id", null); + defer freeJoined(testing.allocator, joined); + + try testing.expectEqual(@as(usize, 3), joined.len); +} + +test "innerJoin: rightPred filters matches out even when the join key matches" { + var tmp = testing.tmpDir(.{}); + defer tmp.cleanup(); + var db = try openTestDb(&tmp); + defer db.close(); + + var customers = try Table.open(&db, testing.allocator, "customers"); + defer customers.deinit(); + try customers.put(.{ .int = 1 }, &.{}); + + var orders = try Table.open(&db, testing.allocator, "orders"); + defer orders.deinit(); + try orders.put(.{ .int = 100 }, &.{ .{ .name = "customer_id", .value = .{ .int = 1 } }, .{ .name = "status", .value = .{ .text = "shipped" } } }); + try orders.put(.{ .int = 101 }, &.{ .{ .name = "customer_id", .value = .{ .int = 1 } }, .{ .name = "status", .value = .{ .text = "cancelled" } } }); + + const shippedOnly = Predicate{ .eq = .{ .field = "status", .value = .{ .text = "shipped" } } }; + const joined = try innerJoin(testing.allocator, &customers, "__pk", null, &orders, "customer_id", shippedOnly); + defer freeJoined(testing.allocator, joined); + + try testing.expectEqual(@as(usize, 1), joined.len); + try testing.expectEqualStrings("shipped", joined[0].right.get("status").?.text); +} + +test "innerJoin: rightPred filters matches out even through an index nested-loop join" { + var tmp = testing.tmpDir(.{}); + defer tmp.cleanup(); + var db = try openTestDb(&tmp); + defer db.close(); + + var customers = try Table.open(&db, testing.allocator, "customers"); + defer customers.deinit(); + try customers.put(.{ .int = 1 }, &.{}); + + var orders = try Table.open(&db, testing.allocator, "orders"); + defer orders.deinit(); + try orders.createIndex("customer_id"); + try orders.put(.{ .int = 100 }, &.{ .{ .name = "customer_id", .value = .{ .int = 1 } }, .{ .name = "status", .value = .{ .text = "shipped" } } }); + try orders.put(.{ .int = 101 }, &.{ .{ .name = "customer_id", .value = .{ .int = 1 } }, .{ .name = "status", .value = .{ .text = "cancelled" } } }); + + const shippedOnly = Predicate{ .eq = .{ .field = "status", .value = .{ .text = "shipped" } } }; + const joined = try innerJoin(testing.allocator, &customers, "__pk", null, &orders, "customer_id", shippedOnly); + defer freeJoined(testing.allocator, joined); + + try testing.expectEqual(@as(usize, 1), joined.len); + try testing.expectEqualStrings("shipped", joined[0].right.get("status").?.text); +} + +test "innerJoin: leftPred narrows which left rows are even probed" { + var tmp = testing.tmpDir(.{}); + defer tmp.cleanup(); + var db = try openTestDb(&tmp); + defer db.close(); + + var customers = try Table.open(&db, testing.allocator, "customers"); + defer customers.deinit(); + try customers.put(.{ .int = 1 }, &.{.{ .name = "vip", .value = .{ .boolean = true } }}); + try customers.put(.{ .int = 2 }, &.{.{ .name = "vip", .value = .{ .boolean = false } }}); + + var orders = try Table.open(&db, testing.allocator, "orders"); + defer orders.deinit(); + try orders.put(.{ .int = 100 }, &.{.{ .name = "customer_id", .value = .{ .int = 1 } }}); + try orders.put(.{ .int = 101 }, &.{.{ .name = "customer_id", .value = .{ .int = 2 } }}); + + const vipOnly = Predicate{ .eq = .{ .field = "vip", .value = .{ .boolean = true } } }; + const joined = try innerJoin(testing.allocator, &customers, "__pk", vipOnly, &orders, "customer_id", null); + defer freeJoined(testing.allocator, joined); + + try testing.expectEqual(@as(usize, 1), joined.len); +} + +test "innerJoin: mismatched types never match even with equal-looking payloads" { + var tmp = testing.tmpDir(.{}); + defer tmp.cleanup(); + var db = try openTestDb(&tmp); + defer db.close(); + + var left = try Table.open(&db, testing.allocator, "left"); + defer left.deinit(); + try left.put(.{ .int = 1 }, &.{.{ .name = "k", .value = .{ .int = 5 } }}); + + var right = try Table.open(&db, testing.allocator, "right"); + defer right.deinit(); + try right.put(.{ .int = 1 }, &.{.{ .name = "k", .value = .{ .float = 5.0 } }}); + + const joined = try innerJoin(testing.allocator, &left, "k", null, &right, "k", null); + defer freeJoined(testing.allocator, joined); + + try testing.expectEqual(@as(usize, 0), joined.len); +} + +test "innerJoin: rows missing the join field on either side never match" { + var tmp = testing.tmpDir(.{}); + defer tmp.cleanup(); + var db = try openTestDb(&tmp); + defer db.close(); + + var left = try Table.open(&db, testing.allocator, "left"); + defer left.deinit(); + try left.put(.{ .int = 1 }, &.{}); // no "k" field at all + try left.put(.{ .int = 2 }, &.{.{ .name = "k", .value = .{ .int = 7 } }}); + + var right = try Table.open(&db, testing.allocator, "right"); + defer right.deinit(); + try right.put(.{ .int = 1 }, &.{.{ .name = "k", .value = .{ .int = 7 } }}); + + const joined = try innerJoin(testing.allocator, &left, "k", null, &right, "k", null); + defer freeJoined(testing.allocator, joined); + + try testing.expectEqual(@as(usize, 1), joined.len); + try testing.expectEqual(@as(i64, 2), joined[0].left.get("__pk").?.int); +} +