feat: add FullOuterJoinedRow and deinit to join
This commit is contained in:
1 parent
16ee4d2e18
commit
03ad32ad97
1 file changed
+151
+151
@@ -313,3 +313,154 @@ pub fn rightOuterJoin(
|
||||
const swapped = try leftOuterJoin(allocator, right, rightField, rightPred, left, leftField, leftPred);
|
||||
defer allocator.free(swapped);
|
||||
|
||||
const results = try allocator.alloc(RightOuterJoinedRow, swapped.len);
|
||||
for (swapped, 0..) |pair, i| {
|
||||
results[i] = .{ .left = pair.right, .right = pair.left };
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/// One row produced by `fullOuterJoin`: both `left` and `right` are
|
||||
/// optional, and every row from either side that never matched on the other
|
||||
/// still appears exactly once (SQL FULL OUTER JOIN semantics - the union of
|
||||
/// what `leftOuterJoin` and `rightOuterJoin` each produce).
|
||||
pub const FullOuterJoinedRow = struct {
|
||||
left: ?Row,
|
||||
right: ?Row,
|
||||
|
||||
pub fn deinit(self: *FullOuterJoinedRow) void {
|
||||
if (self.left) |*l| l.deinit();
|
||||
if (self.right) |*r| r.deinit();
|
||||
}
|
||||
};
|
||||
|
||||
pub fn freeFullOuterJoined(allocator: Allocator, rows: []FullOuterJoinedRow) void {
|
||||
for (rows) |*r| r.deinit();
|
||||
allocator.free(rows);
|
||||
}
|
||||
|
||||
/// Full outer equi-join: the union of `leftOuterJoin` (every left row,
|
||||
/// matched or not) and every right row `rightOuterJoin` reports as
|
||||
/// unmatched (`left == null`) - those are exactly the right-side rows a
|
||||
/// plain left outer join would never produce. Built from the two existing
|
||||
/// outer joins rather than a third independent matching pass, at the
|
||||
/// honest cost of computing the match twice (once per direction) - the
|
||||
/// same "correctness over a second matching pass" tradeoff already used by
|
||||
/// `rank`/`rangeByRank` in collections.zig.
|
||||
///
|
||||
/// Caller owns the returned slice and every `FullOuterJoinedRow` in it;
|
||||
/// free with `freeFullOuterJoined`.
|
||||
pub fn fullOuterJoin(
|
||||
allocator: Allocator,
|
||||
left: *Table,
|
||||
leftField: []const u8,
|
||||
leftPred: ?Predicate,
|
||||
right: *Table,
|
||||
rightField: []const u8,
|
||||
rightPred: ?Predicate,
|
||||
) ![]FullOuterJoinedRow {
|
||||
const leftSide = try leftOuterJoin(allocator, left, leftField, leftPred, right, rightField, rightPred);
|
||||
errdefer freeOuterJoined(allocator, leftSide);
|
||||
|
||||
const rightSide = try rightOuterJoin(allocator, left, leftField, leftPred, right, rightField, rightPred);
|
||||
errdefer freeRightOuterJoined(allocator, rightSide);
|
||||
|
||||
var unmatchedRightCount: usize = 0;
|
||||
for (rightSide) |pair| {
|
||||
if (pair.left == null) unmatchedRightCount += 1;
|
||||
}
|
||||
|
||||
var results = try std.ArrayList(FullOuterJoinedRow).initCapacity(allocator, leftSide.len + unmatchedRightCount);
|
||||
errdefer {
|
||||
for (results.items) |*r| r.deinit();
|
||||
results.deinit();
|
||||
}
|
||||
|
||||
for (leftSide) |pair| {
|
||||
results.appendAssumeCapacity(.{ .left = pair.left, .right = pair.right });
|
||||
}
|
||||
allocator.free(leftSide);
|
||||
|
||||
for (rightSide) |pair| {
|
||||
if (pair.left) |l| {
|
||||
var lm = l;
|
||||
lm.deinit();
|
||||
var rm = pair.right;
|
||||
rm.deinit();
|
||||
} else {
|
||||
results.appendAssumeCapacity(.{ .left = null, .right = pair.right });
|
||||
}
|
||||
}
|
||||
allocator.free(rightSide);
|
||||
|
||||
return results.toOwnedSlice();
|
||||
}
|
||||
|
||||
/// Trims one of the four join result slices above to a `query.zig` `Page`
|
||||
/// window (0 = unlimited, the same convention `Table.selectPage` uses) -
|
||||
/// the join counterpart of pagination for `table_join`/`remote-join`.
|
||||
/// Unlike `Table.selectPage`, which narrows an index scan and can stop
|
||||
/// before materializing a row past the limit, a join has already had to
|
||||
/// produce a full pair to know it matched at all, so there is no cheaper
|
||||
/// way to honor `offset`/`limit` than windowing the already-complete
|
||||
/// result - this always runs after `innerJoin`/`leftOuterJoin`/
|
||||
/// `rightOuterJoin`/`fullOuterJoin` returns, never instead of it.
|
||||
///
|
||||
/// Takes ownership of `rows`: frees the backing slice, `.deinit()`s every
|
||||
/// pair that falls outside `[offset, offset+limit)`, and returns a freshly
|
||||
/// allocated slice holding only the kept pairs in their original order.
|
||||
/// Callers own the returned slice and the pairs in it exactly as they
|
||||
/// would have owned `rows` - free the pairs, then `allocator.free` the
|
||||
/// slice (or hand ownership onward, as `server.zig`'s `table_join` handler
|
||||
/// does by moving each kept pair into a `protocol.JoinPair`).
|
||||
pub fn applyPage(comptime T: type, allocator: Allocator, rows: []T, page: querymod.Page) ![]T {
|
||||
const total = rows.len;
|
||||
const start = @min(page.offset, total);
|
||||
const end = if (page.limit == 0) total else @min(start + page.limit, total);
|
||||
|
||||
for (rows[0..start]) |*r| r.deinit();
|
||||
for (rows[end..total]) |*r| r.deinit();
|
||||
|
||||
const kept = end - start;
|
||||
const result = allocator.alloc(T, kept) catch |err| {
|
||||
for (rows[start..end]) |*r| r.deinit();
|
||||
allocator.free(rows);
|
||||
return err;
|
||||
};
|
||||
@memcpy(result, rows[start..end]);
|
||||
allocator.free(rows);
|
||||
return result;
|
||||
}
|
||||
|
||||
const testing = std.testing;
|
||||
const dbmod = @import("db.zig");
|
||||
|
||||
fn openTestDb(tmp: *std.testing.TmpDir) !dbmod.Db {
|
||||
const dbPath = try tmp.dir.realpathAlloc(testing.allocator, ".");
|
||||
defer testing.allocator.free(dbPath);
|
||||
return dbmod.Db.open(testing.allocator, dbPath, .{});
|
||||
}
|
||||
|
||||
test "innerJoin: unindexed hash join matches on equal field value" {
|
||||
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.put(.{ .int = 100 }, &.{ .{ .name = "customer_id", .value = .{ .int = 1 } }, .{ .name = "item", .value = .{ .text = "widget" } } });
|
||||
try orders.put(.{ .int = 101 }, &.{ .{ .name = "customer_id", .value = .{ .int = 1 } }, .{ .name = "item", .value = .{ .text = "gadget" } } });
|
||||
try orders.put(.{ .int = 102 }, &.{ .{ .name = "customer_id", .value = .{ .int = 2 } }, .{ .name = "item", .value = .{ .text = "gizmo" } } });
|
||||
try orders.put(.{ .int = 103 }, &.{.{ .name = "customer_id", .value = .{ .int = 999 } }}); // no matching customer
|
||||
|
||||
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);
|
||||
|
||||
Reference in new issue
Block a user