feat: build out join
This commit is contained in:
1 parent
c03d2ad2cd
commit
453b21c01d
1 file changed
+157
+157
@@ -783,3 +783,160 @@ test "leftOuterJoin: left row missing the join field entirely still appears once
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "leftOuterJoin: one-to-many fan-out on the matched side still produces one row per 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 }, &.{.{ .name = "k", .value = .{ .int = 42 } }});
|
||||||
|
|
||||||
|
var right = try Table.open(&db, testing.allocator, "right");
|
||||||
|
defer right.deinit();
|
||||||
|
var i: i64 = 0;
|
||||||
|
while (i < 5) : (i += 1) {
|
||||||
|
try right.put(.{ .int = i }, &.{.{ .name = "k", .value = .{ .int = 42 } }});
|
||||||
|
}
|
||||||
|
|
||||||
|
const joined = try leftOuterJoin(testing.allocator, &left, "k", null, &right, "k", null);
|
||||||
|
defer freeOuterJoined(testing.allocator, joined);
|
||||||
|
|
||||||
|
try testing.expectEqual(@as(usize, 5), joined.len);
|
||||||
|
for (joined) |pair| {
|
||||||
|
try testing.expect(pair.right != null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "leftOuterJoin: empty right table yields exactly one unmatched row per left row" {
|
||||||
|
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 = 1 } }});
|
||||||
|
try left.put(.{ .int = 2 }, &.{.{ .name = "k", .value = .{ .int = 2 } }});
|
||||||
|
|
||||||
|
var right = try Table.open(&db, testing.allocator, "right");
|
||||||
|
defer right.deinit();
|
||||||
|
|
||||||
|
const joined = try leftOuterJoin(testing.allocator, &left, "k", null, &right, "k", null);
|
||||||
|
defer freeOuterJoined(testing.allocator, joined);
|
||||||
|
|
||||||
|
try testing.expectEqual(@as(usize, 2), joined.len);
|
||||||
|
for (joined) |pair| {
|
||||||
|
try testing.expect(pair.right == null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "rightOuterJoin: unmatched right row survives with left=null instead of vanishing" {
|
||||||
|
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" } }});
|
||||||
|
|
||||||
|
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 = 999 } }}); // no matching customer
|
||||||
|
|
||||||
|
const joined = try rightOuterJoin(testing.allocator, &customers, "__pk", null, &orders, "customer_id", null);
|
||||||
|
defer freeRightOuterJoined(testing.allocator, joined);
|
||||||
|
|
||||||
|
try testing.expectEqual(@as(usize, 2), joined.len);
|
||||||
|
var sawMatched = false;
|
||||||
|
var sawUnmatched = false;
|
||||||
|
for (joined) |pair| {
|
||||||
|
const customerId = pair.right.get("customer_id").?.int;
|
||||||
|
if (customerId == 1) {
|
||||||
|
try testing.expect(pair.left != null);
|
||||||
|
try testing.expectEqualStrings("alice", pair.left.?.get("name").?.text);
|
||||||
|
sawMatched = true;
|
||||||
|
} else {
|
||||||
|
try testing.expect(pair.left == null);
|
||||||
|
sawUnmatched = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try testing.expect(sawMatched);
|
||||||
|
try testing.expect(sawUnmatched);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "rightOuterJoin: index nested-loop join (index on left table) matches hash join results" {
|
||||||
|
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.createIndex("__pk");
|
||||||
|
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 } }});
|
||||||
|
try orders.put(.{ .int = 101 }, &.{.{ .name = "customer_id", .value = .{ .int = 2 } }}); // unmatched
|
||||||
|
|
||||||
|
const joined = try rightOuterJoin(testing.allocator, &customers, "__pk", null, &orders, "customer_id", null);
|
||||||
|
defer freeRightOuterJoined(testing.allocator, joined);
|
||||||
|
|
||||||
|
try testing.expectEqual(@as(usize, 2), joined.len);
|
||||||
|
var unmatchedCount: usize = 0;
|
||||||
|
for (joined) |pair| {
|
||||||
|
if (pair.left == null) unmatchedCount += 1;
|
||||||
|
}
|
||||||
|
try testing.expectEqual(@as(usize, 1), unmatchedCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "rightOuterJoin: leftPred filtering out the only match still keeps the right row with left=null" {
|
||||||
|
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 = "active", .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 } }});
|
||||||
|
|
||||||
|
const activeOnly = Predicate{ .eq = .{ .field = "active", .value = .{ .boolean = true } } };
|
||||||
|
const joined = try rightOuterJoin(testing.allocator, &customers, "__pk", activeOnly, &orders, "customer_id", null);
|
||||||
|
defer freeRightOuterJoined(testing.allocator, joined);
|
||||||
|
|
||||||
|
try testing.expectEqual(@as(usize, 1), joined.len);
|
||||||
|
try testing.expect(joined[0].left == null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "rightOuterJoin: empty left table yields exactly one unmatched row per right row" {
|
||||||
|
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();
|
||||||
|
|
||||||
|
var right = try Table.open(&db, testing.allocator, "right");
|
||||||
|
defer right.deinit();
|
||||||
|
try right.put(.{ .int = 1 }, &.{.{ .name = "k", .value = .{ .int = 1 } }});
|
||||||
|
try right.put(.{ .int = 2 }, &.{.{ .name = "k", .value = .{ .int = 2 } }});
|
||||||
|
|
||||||
|
const joined = try rightOuterJoin(testing.allocator, &left, "k", null, &right, "k", null);
|
||||||
|
defer freeRightOuterJoined(testing.allocator, joined);
|
||||||
|
|
||||||
|
try testing.expectEqual(@as(usize, 2), joined.len);
|
||||||
|
for (joined) |pair| {
|
||||||
|
try testing.expect(pair.left == null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in new issue
Block a user