const std = @import("std"); const rowmod = @import("row.zig"); const querymod = @import("query.zig"); const Allocator = std.mem.Allocator; const Row = rowmod.Row; const Table = querymod.Table; const Predicate = querymod.Predicate; /// One matched pair produced by `innerJoin`: the left table's row and the /// right table's row that satisfied `leftRow.get(leftField) == /// rightRow.get(rightField)`. Kept as two separate owned Rows rather than /// merged into one flat namespace, so join output never has to invent a /// collision rule for a field name that happens to exist on both sides. pub const JoinedRow = struct { left: Row, right: Row, pub fn deinit(self: *JoinedRow) void { self.left.deinit(); self.right.deinit(); } }; pub fn freeJoined(allocator: Allocator, rows: []JoinedRow) void { for (rows) |*r| r.deinit(); allocator.free(rows); } /// Independent copy of `row` via its own encode/decode round trip, so a /// join result can outlive (and be freed separately from) the pooled /// `Table.select` slice it was matched out of - the same row can appear in /// more than one output pair on a one-to-many join. fn dupeRow(allocator: Allocator, row: *const Row) !Row { const buf = try row.encode(allocator); defer allocator.free(buf); return try Row.decode(allocator, buf); } /// Inner equi-join between `left` and `right` on /// `leftRow.get(leftField) == rightRow.get(rightField)` (compared with /// `row.compare`, so a mismatched type - e.g. an int on one side against a /// float on the other - never matches, exactly like `Predicate.eq`). /// `leftPred`/`rightPred` filter each side first, evaluated exactly like /// `Table.select` (including its own index usage). A row missing the join /// field entirely never matches anything, on either side. /// /// Join strategy is chosen once, not per call site: if `right` already has /// a secondary index on `rightField`, this runs an index nested-loop join - /// one `right.select` per surviving left row, narrowed through that index /// exactly the way a plain `eq` query would be. Otherwise it builds an /// in-memory hash table over the (already `rightPred`-filtered) right rows /// keyed by the join value and probes it once per left row - a hash join, /// so an unindexed join over two large tables still costs /// O(leftRows + rightRows) rather than O(leftRows * rightRows). /// /// Caller owns the returned slice and every `JoinedRow` in it; free with /// `freeJoined`. pub fn innerJoin( allocator: Allocator, left: *Table, leftField: []const u8, leftPred: ?Predicate, right: *Table, rightField: []const u8, rightPred: ?Predicate, ) ![]JoinedRow { const leftRows = try left.select(leftPred); defer querymod.freeRows(allocator, leftRows); var results = std.ArrayList(JoinedRow).init(allocator); errdefer { for (results.items) |*r| r.deinit(); results.deinit(); } if (right.hasIndex(rightField)) { for (leftRows) |*lrow| { const v = lrow.get(leftField) orelse 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); 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 continue; const key = try rowmod.encodeValue(allocator, v); defer allocator.free(key); const indices = buckets.get(key) orelse 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 `leftOuterJoin`: every left row appears at least /// once, with `right` set to `null` when nothing on the right side matched /// (SQL LEFT OUTER JOIN semantics - the left side is never dropped, unlike /// `innerJoin` where an unmatched left row disappears entirely). pub const OuterJoinedRow = struct { left: Row, right: ?Row, pub fn deinit(self: *OuterJoinedRow) void { self.left.deinit(); if (self.right) |*r| r.deinit(); } }; pub fn freeOuterJoined(allocator: Allocator, rows: []OuterJoinedRow) void { for (rows) |*r| r.deinit(); allocator.free(rows); }