feat: add openFourPairJoin and customerNames to join
This commit is contained in:
1 parent
453b21c01d
commit
6b7de3edb7
1 file changed
+216
+216
@@ -940,3 +940,219 @@ test "rightOuterJoin: empty left table yields exactly one unmatched row per righ
|
||||
}
|
||||
}
|
||||
|
||||
test "rightOuterJoin: 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 rightOuterJoin(testing.allocator, &left, "k", null, &right, "k", null);
|
||||
defer freeRightOuterJoined(testing.allocator, joined);
|
||||
|
||||
try testing.expectEqual(@as(usize, 5), joined.len);
|
||||
for (joined) |pair| {
|
||||
try testing.expect(pair.left != null);
|
||||
}
|
||||
}
|
||||
|
||||
test "fullOuterJoin: rows unmatched on either side survive, matched pairs appear once" {
|
||||
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" } }}); // matches
|
||||
try customers.put(.{ .int = 2 }, &.{.{ .name = "name", .value = .{ .text = "bob" } }}); // never ordered
|
||||
|
||||
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 } }}); // orphan order
|
||||
|
||||
const joined = try fullOuterJoin(testing.allocator, &customers, "__pk", null, &orders, "customer_id", null);
|
||||
defer freeFullOuterJoined(testing.allocator, joined);
|
||||
|
||||
// alice+order100 (matched), bob+null (left-only), null+order101 (right-only).
|
||||
try testing.expectEqual(@as(usize, 3), joined.len);
|
||||
var sawMatched = false;
|
||||
var sawLeftOnly = false;
|
||||
var sawRightOnly = false;
|
||||
for (joined) |pair| {
|
||||
if (pair.left != null and pair.right != null) {
|
||||
try testing.expectEqualStrings("alice", pair.left.?.get("name").?.text);
|
||||
sawMatched = true;
|
||||
} else if (pair.left != null and pair.right == null) {
|
||||
try testing.expectEqualStrings("bob", pair.left.?.get("name").?.text);
|
||||
sawLeftOnly = true;
|
||||
} else if (pair.left == null and pair.right != null) {
|
||||
try testing.expectEqual(@as(i64, 999), pair.right.?.get("customer_id").?.int);
|
||||
sawRightOnly = true;
|
||||
} else {
|
||||
try testing.expect(false); // unreachable: both sides null never happens
|
||||
}
|
||||
}
|
||||
try testing.expect(sawMatched);
|
||||
try testing.expect(sawLeftOnly);
|
||||
try testing.expect(sawRightOnly);
|
||||
}
|
||||
|
||||
test "fullOuterJoin: fully disjoint tables yield exactly one unmatched row per side" {
|
||||
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 } }});
|
||||
|
||||
var right = try Table.open(&db, testing.allocator, "right");
|
||||
defer right.deinit();
|
||||
try right.put(.{ .int = 1 }, &.{.{ .name = "k", .value = .{ .int = 2 } }});
|
||||
|
||||
const joined = try fullOuterJoin(testing.allocator, &left, "k", null, &right, "k", null);
|
||||
defer freeFullOuterJoined(testing.allocator, joined);
|
||||
|
||||
try testing.expectEqual(@as(usize, 2), joined.len);
|
||||
var leftOnly: usize = 0;
|
||||
var rightOnly: usize = 0;
|
||||
for (joined) |pair| {
|
||||
if (pair.left != null and pair.right == null) leftOnly += 1;
|
||||
if (pair.left == null and pair.right != null) rightOnly += 1;
|
||||
}
|
||||
try testing.expectEqual(@as(usize, 1), leftOnly);
|
||||
try testing.expectEqual(@as(usize, 1), rightOnly);
|
||||
}
|
||||
|
||||
test "fullOuterJoin: both tables empty yields zero rows" {
|
||||
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();
|
||||
|
||||
const joined = try fullOuterJoin(testing.allocator, &left, "k", null, &right, "k", null);
|
||||
defer freeFullOuterJoined(testing.allocator, joined);
|
||||
|
||||
try testing.expectEqual(@as(usize, 0), joined.len);
|
||||
}
|
||||
|
||||
fn openFourPairJoin(tmp: *std.testing.TmpDir, db: *dbmod.Db) ![]JoinedRow {
|
||||
var customers = try Table.open(db, testing.allocator, "customers");
|
||||
defer customers.deinit();
|
||||
var orders = try Table.open(db, testing.allocator, "orders");
|
||||
defer orders.deinit();
|
||||
|
||||
var i: i64 = 1;
|
||||
while (i <= 4) : (i += 1) {
|
||||
try customers.put(.{ .int = i }, &.{.{ .name = "name", .value = .{ .int = i } }});
|
||||
try orders.put(.{ .int = i * 100 }, &.{.{ .name = "customer_id", .value = .{ .int = i } }});
|
||||
}
|
||||
_ = tmp;
|
||||
return innerJoin(testing.allocator, &customers, "__pk", null, &orders, "customer_id", null);
|
||||
}
|
||||
|
||||
fn customerNames(pairs: []const JoinedRow) [4]i64 {
|
||||
var out: [4]i64 = undefined;
|
||||
for (pairs, 0..) |p, i| out[i] = p.left.get("name").?.int;
|
||||
return out;
|
||||
}
|
||||
|
||||
test "applyPage with offset and limit keeps a middle window in order" {
|
||||
var tmp = testing.tmpDir(.{});
|
||||
defer tmp.cleanup();
|
||||
var db = try openTestDb(&tmp);
|
||||
defer db.close();
|
||||
|
||||
const joined = try openFourPairJoin(&tmp, &db);
|
||||
try testing.expectEqual(@as(usize, 4), joined.len);
|
||||
|
||||
const paged = try applyPage(JoinedRow, testing.allocator, joined, .{ .offset = 1, .limit = 2 });
|
||||
defer freeJoined(testing.allocator, paged);
|
||||
|
||||
try testing.expectEqual(@as(usize, 2), paged.len);
|
||||
try testing.expectEqual(@as(i64, 2), paged[0].left.get("name").?.int);
|
||||
try testing.expectEqual(@as(i64, 3), paged[1].left.get("name").?.int);
|
||||
}
|
||||
|
||||
test "applyPage with limit 0 returns every row, matching unlimited select" {
|
||||
var tmp = testing.tmpDir(.{});
|
||||
defer tmp.cleanup();
|
||||
var db = try openTestDb(&tmp);
|
||||
defer db.close();
|
||||
|
||||
const joined = try openFourPairJoin(&tmp, &db);
|
||||
const paged = try applyPage(JoinedRow, testing.allocator, joined, .{});
|
||||
defer freeJoined(testing.allocator, paged);
|
||||
|
||||
try testing.expectEqualSlices(i64, &.{ 1, 2, 3, 4 }, &customerNames(paged));
|
||||
}
|
||||
|
||||
test "applyPage with offset past the end returns nothing" {
|
||||
var tmp = testing.tmpDir(.{});
|
||||
defer tmp.cleanup();
|
||||
var db = try openTestDb(&tmp);
|
||||
defer db.close();
|
||||
|
||||
const joined = try openFourPairJoin(&tmp, &db);
|
||||
const paged = try applyPage(JoinedRow, testing.allocator, joined, .{ .offset = 50 });
|
||||
defer freeJoined(testing.allocator, paged);
|
||||
|
||||
try testing.expectEqual(@as(usize, 0), paged.len);
|
||||
}
|
||||
|
||||
test "applyPage with limit larger than the remaining rows stops at the end" {
|
||||
var tmp = testing.tmpDir(.{});
|
||||
defer tmp.cleanup();
|
||||
var db = try openTestDb(&tmp);
|
||||
defer db.close();
|
||||
|
||||
const joined = try openFourPairJoin(&tmp, &db);
|
||||
const paged = try applyPage(JoinedRow, testing.allocator, joined, .{ .offset = 3, .limit = 100 });
|
||||
defer freeJoined(testing.allocator, paged);
|
||||
|
||||
try testing.expectEqual(@as(usize, 1), paged.len);
|
||||
try testing.expectEqual(@as(i64, 4), paged[0].left.get("name").?.int);
|
||||
}
|
||||
|
||||
test "applyPage propagates OutOfMemory from its own result allocation without leaking any row" {
|
||||
var tmp = testing.tmpDir(.{});
|
||||
defer tmp.cleanup();
|
||||
var db = try openTestDb(&tmp);
|
||||
defer db.close();
|
||||
|
||||
// `joined` itself is built with the real `testing.allocator` - every
|
||||
// `Row` inside stores that same allocator (see `row.zig`'s `Row.init`),
|
||||
// so `applyPage`'s error path frees each one through it regardless of
|
||||
// which allocator `applyPage` was asked to allocate its OWN result
|
||||
// slice with below. Isolates the OOM path to exactly one call site:
|
||||
// the `allocator.alloc(T, kept)` in `applyPage` itself, not the join
|
||||
// that already succeeded.
|
||||
const joined = try openFourPairJoin(&tmp, &db);
|
||||
try testing.expectEqual(@as(usize, 4), joined.len);
|
||||
|
||||
var failing = testing.FailingAllocator.init(testing.allocator, .{ .fail_index = 0 });
|
||||
try testing.expectError(error.OutOfMemory, applyPage(JoinedRow, failing.allocator(), joined, .{ .offset = 1, .limit = 2 }));
|
||||
// No explicit assertion beyond this: `testing.allocator`'s leak
|
||||
// detector at process exit is the real check - every one of the 4
|
||||
// `JoinedRow`s (2 kept by the window, 2 trimmed outside it) must have
|
||||
// been `.deinit()`-ed on the error path in `applyPage`, and the
|
||||
// `rows` backing slice itself freed, or this test fails on leak.
|
||||
}
|
||||
Reference in new issue
Block a user