╭ changed ────╮ │src/event.zig│ ╰─────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │287 const baseline_haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &baseline_oid)) orelse return error.CursorNotFound;│ │288 const baseline_haxy_moment = try DB.HashMap(.read_only).init(baseline_haxy_moment_cursor); │ │289 │ │290 - try mergeChangedMapEntries(DB, haxy_moment, parent_haxy_moment, baseline_haxy_moment, true); │ │290 + try mergeChangedMapEntries(DB, allocator, haxy_moment, parent_haxy_moment, baseline_haxy_moment, true); │ │291 } │ │292 } │ │293 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────╮ │566 │ │567 fn mergeChangedMapEntries( │ │568 comptime DB: type, │ │569 + allocator: std.mem.Allocator, │ │570 target: DB.HashMap(.read_write), │ │571 parent: DB.HashMap(.read_only), │ │572 baseline: DB.HashMap(.read_only),│ ╰──────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │598 if (tag == .hash_map) { │ │599 const target_existing_cursor = try target.getCursor(kv_pair.hash) orelse return error.MergeConflict;│ │600 if (target_existing_cursor.slot().tag != .hash_map) { │ │600 - return error.MergeConflict; │ │601 + return error.UnexpectedTag; │ │602 } │ │603 │ │604 const target_child_cursor = try target.putCursor(kv_pair.hash); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │605 const target_child = try DB.HashMap(.read_write).init(target_child_cursor); │ │606 const parent_child = try DB.HashMap(.read_only).init(kv_pair.value_cursor); │ │607 const baseline_child = try DB.HashMap(.read_only).init(baseline_value_cursor); │ │607 - try mergeChangedMapEntries(DB, target_child, parent_child, baseline_child, false); │ │608 + try mergeChangedMapEntries(DB, allocator, target_child, parent_child, baseline_child, false); │ │609 + } else if (tag == .sorted_set) { │ │610 + const target_existing_cursor = try target.getCursor(kv_pair.hash) orelse return error.MergeConflict;│ │611 + if (target_existing_cursor.slot().tag != .sorted_set) { │ │612 + return error.UnexpectedTag; │ │613 + } │ │614 + │ │615 + const target_child_cursor = try target.putCursor(kv_pair.hash); │ │616 + const target_child = try DB.SortedSet(.read_write).init(target_child_cursor); │ │617 + const parent_child = try DB.SortedSet(.read_only).init(kv_pair.value_cursor); │ │618 + const baseline_child = try DB.SortedSet(.read_only).init(baseline_value_cursor); │ │619 + try mergeChangedSetEntries(DB, allocator, target_child, parent_child, baseline_child); │ │620 } else if (is_top_level) { │ │621 // the moment-index is a top-level key in the haxy moment │ │622 // and is not a map. we don't want to do any merge of this. │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │623 continue; │ │624 } else { │ │625 const target_value_cursor = try target.getCursor(kv_pair.hash) orelse return error.MergeConflict;│ │626 + if (target_value_cursor.slot().tag != baseline_value_cursor.slot().tag) { │ │627 + return error.UnexpectedTag; │ │628 + } │ │629 if (target_value_cursor.slot().value != baseline_value_cursor.slot().value) { │ │630 return error.MergeConflict; │ │631 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │635 } │ │636 } │ │637 │ │638 +// three-way merge of a sorted set: apply the parent's changes relative to the│ │639 +// baseline. keys have no values, so additions and removals can't conflict. │ │640 +fn mergeChangedSetEntries( │ │641 + comptime DB: type, │ │642 + allocator: std.mem.Allocator, │ │643 + target: DB.SortedSet(.read_write), │ │644 + parent: DB.SortedSet(.read_only), │ │645 + baseline: DB.SortedSet(.read_only), │ │646 +) !void { │ │647 + var parent_iter = try parent.iterator(); │ │648 + while (try parent_iter.next()) |kv_pair_cursor| { │ │649 + const kv_pair = try kv_pair_cursor.readKeyValuePair(); │ │650 + const key = try kv_pair.key_cursor.readBytesAlloc(allocator, null); │ │651 + defer allocator.free(key); │ │652 + if (!try baseline.contains(key)) try target.put(key); │ │653 + } │ │654 + │ │655 + var baseline_iter = try baseline.iterator(); │ │656 + while (try baseline_iter.next()) |kv_pair_cursor| { │ │657 + const kv_pair = try kv_pair_cursor.readKeyValuePair(); │ │658 + const key = try kv_pair.key_cursor.readBytesAlloc(allocator, null); │ │659 + defer allocator.free(key); │ │660 + if (!try parent.contains(key)) _ = try target.remove(key); │ │661 + } │ │662 +} │ │663 + │ │664 fn upsertField( │ │665 comptime DB: type, │ │666 comptime hash_kind: hash.HashKind, │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────╮ │src/test/event.zig│ ╰──────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │498 │ │499 try std.testing.expectEqualStrings(events_to_consume2[0].event.issue.?.description, first_issue.description); │ │500 try std.testing.expectEqualStrings(events_to_consume2[0].event.issue.?.tags, first_issue.tags); │ │501 + } │ │502 + │ │503 + // the ordered issue set unions both branches' additions │ │504 + { │ │505 + const issue_id_set_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "issue-id-set")) orelse return error.NotFound;│ │506 + const issue_id_set = try Repo.DB.SortedSet(.read_only).init(issue_id_set_cursor); │ │507 + try std.testing.expectEqual(6, try issue_id_set.count()); │ │508 + │ │509 + var found = [_]bool{false} ** 6; │ │510 + var iter = try issue_id_set.iteratorFromIndex(0); │ │511 + while (try iter.next()) |id_cursor_val| { │ │512 + var id_cursor = id_cursor_val; │ │513 + const id_kv = try id_cursor.readKeyValuePair(); │ │514 + var order_key: [@sizeOf(u64) + evt.event_id_size]u8 = undefined; │ │515 + _ = try id_kv.key_cursor.readBytes(&order_key); │ │516 + const id_hex = std.fmt.bytesToHex(order_key[@sizeOf(u64)..].*, .lower); │ │517 + for (events_to_consume, 0..) |event, i| { │ │518 + if (std.mem.eql(u8, &id_hex, &event.id)) found[i] = true; │ │519 + } │ │520 + for (events_to_consume2, 0..) |event, i| { │ │521 + if (std.mem.eql(u8, &id_hex, &event.id)) found[events_to_consume.len + i] = true; │ │522 + } │ │523 + } │ │524 + for (found) |f| try std.testing.expect(f); │ │525 } │ │526 } │ │527 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯