feat: add leftOuterJoin and RightOuterJoinedRow to join
This commit is contained in:
1 parent
a461688a7d
commit
16ee4d2e18
1 file changed
+156
+156
@@ -157,3 +157,159 @@ pub fn freeOuterJoined(allocator: Allocator, rows: []OuterJoinedRow) void {
|
|||||||
allocator.free(rows);
|
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);
|
||||||
|
|
||||||
Reference in new issue
Block a user