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);
|
const swapped = try leftOuterJoin(allocator, right, rightField, rightPred, left, leftField, leftPred);
|
||||||
defer allocator.free(swapped);
|
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