feat: add leftOuterJoin and RightOuterJoinedRow to join

This commit is contained in:
dualfroz committed 2026-09-06 14:58:37 +02:00
1 parent a461688a7d
commit 16ee4d2e18
1 file changed
+156
+156
View File
@@ -157,3 +157,159 @@ pub fn freeOuterJoined(allocator: Allocator, rows: []OuterJoinedRow) void {
allocator.free(rows);
}
/// Left outer equi-join: same matching rules, strategy selection (index
/// nested-loop vs. hash join) and predicate semantics as `innerJoin`, but a
/// left row with zero matches on the right (missing join field, filtered
/// out by `rightPred`, or genuinely no equal value on the right) still
/// produces exactly one `OuterJoinedRow` with `right = null` instead of
/// vanishing from the output.
///
/// Caller owns the returned slice and every `OuterJoinedRow` in it; free
/// with `freeOuterJoined`.
pub fn leftOuterJoin(
allocator: Allocator,
left: *Table,
leftField: []const u8,
leftPred: ?Predicate,
right: *Table,
rightField: []const u8,
rightPred: ?Predicate,
) ![]OuterJoinedRow {
const leftRows = try left.select(leftPred);
defer querymod.freeRows(allocator, leftRows);
var results = std.ArrayList(OuterJoinedRow).init(allocator);
errdefer {
for (results.items) |*r| r.deinit();
results.deinit();
}
if (right.hasIndex(rightField)) {
for (leftRows) |*lrow| {
const v = lrow.get(leftField) orelse {
var lcopy = try dupeRow(allocator, lrow);
errdefer lcopy.deinit();
try results.append(.{ .left = lcopy, .right = null });
continue;
};
const eqPred = Predicate{ .eq = .{ .field = rightField, .value = v } };
var combinedBuf: [2]Predicate = undefined;
const pred: Predicate = if (rightPred) |rp| blk: {
combinedBuf[0] = eqPred;
combinedBuf[1] = rp;
break :blk Predicate{ .and_ = combinedBuf[0..] };
} else eqPred;
const matches = try right.select(pred);
defer querymod.freeRows(allocator, matches);
if (matches.len == 0) {
var lcopy = try dupeRow(allocator, lrow);
errdefer lcopy.deinit();
try results.append(.{ .left = lcopy, .right = null });
continue;
}
for (matches) |*mrow| {
var lcopy = try dupeRow(allocator, lrow);
errdefer lcopy.deinit();
const rcopy = try dupeRow(allocator, mrow);
try results.append(.{ .left = lcopy, .right = rcopy });
}
}
return results.toOwnedSlice();
}
const rightRows = try right.select(rightPred);
defer querymod.freeRows(allocator, rightRows);
var buckets = std.StringHashMap(std.ArrayList(usize)).init(allocator);
defer {
var it = buckets.iterator();
while (it.next()) |entry| {
entry.value_ptr.deinit();
allocator.free(entry.key_ptr.*);
}
buckets.deinit();
}
for (rightRows, 0..) |*rrow, i| {
const v = rrow.get(rightField) orelse continue;
const key = try rowmod.encodeValue(allocator, v);
const gop = try buckets.getOrPut(key);
if (gop.found_existing) {
allocator.free(key);
} else {
gop.value_ptr.* = std.ArrayList(usize).init(allocator);
}
try gop.value_ptr.append(i);
}
for (leftRows) |*lrow| {
const v = lrow.get(leftField) orelse {
var lcopy = try dupeRow(allocator, lrow);
errdefer lcopy.deinit();
try results.append(.{ .left = lcopy, .right = null });
continue;
};
const key = try rowmod.encodeValue(allocator, v);
defer allocator.free(key);
const indices = buckets.get(key) orelse {
var lcopy = try dupeRow(allocator, lrow);
errdefer lcopy.deinit();
try results.append(.{ .left = lcopy, .right = null });
continue;
};
for (indices.items) |i| {
var lcopy = try dupeRow(allocator, lrow);
errdefer lcopy.deinit();
const rcopy = try dupeRow(allocator, &rightRows[i]);
try results.append(.{ .left = lcopy, .right = rcopy });
}
}
return results.toOwnedSlice();
}
/// One row produced by `rightOuterJoin`: every right row appears at least
/// once, with `left` set to `null` when nothing on the left side matched
/// (SQL RIGHT OUTER JOIN semantics - the mirror image of `OuterJoinedRow`,
/// where it is the right side that is optional).
pub const RightOuterJoinedRow = struct {
left: ?Row,
right: Row,
pub fn deinit(self: *RightOuterJoinedRow) void {
if (self.left) |*l| l.deinit();
self.right.deinit();
}
};
pub fn freeRightOuterJoined(allocator: Allocator, rows: []RightOuterJoinedRow) void {
for (rows) |*r| r.deinit();
allocator.free(rows);
}
/// Right outer equi-join: same matching rules and predicate semantics as
/// `innerJoin`/`leftOuterJoin`, but every `right` row appears at least once,
/// with `left = null` when nothing on the left side matched.
///
/// Implemented by delegating to `leftOuterJoin` with the two sides swapped
/// and remapping the result, rather than re-deriving the index/hash-join
/// strategy selection a third time - `leftOuterJoin(right, ..., left, ...)`
/// already produces exactly "every row of `right`, paired with an optional
/// match from `left`", which is right outer join semantics with the two
/// output fields named the other way round.
///
/// Caller owns the returned slice and every `RightOuterJoinedRow` in it;
/// free with `freeRightOuterJoined`.
pub fn rightOuterJoin(
allocator: Allocator,
left: *Table,
leftField: []const u8,
leftPred: ?Predicate,
right: *Table,
rightField: []const u8,
rightPred: ?Predicate,
) ![]RightOuterJoinedRow {
const swapped = try leftOuterJoin(allocator, right, rightField, rightPred, left, leftField, leftPred);
defer allocator.free(swapped);