diff --git a/src/join.zig b/src/join.zig index 98ed6a9..73f44a9 100644 --- a/src/join.zig +++ b/src/join.zig @@ -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); +