╭ changed ─────────╮ │src/test/event.zig│ ╰──────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────╮ │609 // │ │610 // insert issues as commits in the repo on different branches │ │611 // │ │612 + │ │613 + var conflicting_oids: [events_to_consume3.len][hash.byteLen(repo_opts.hash)]u8 = undefined;│ │614 │ │615 { │ │616 var json: std.Io.Writer.Allocating = .init(allocator); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │622 try std.json.Stringify.value(event, .{}, &json.writer); │ │623 │ │624 // commit the event into a special branch │ │623 - _ = try repo.commitAtRef( │ │625 + const oid = try repo.commitAtRef( │ │626 io, │ │627 allocator, │ │628 .{ .parent_oids = &.{merge_oid}, .message = json.written() },│ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────╮ │633 else => unreachable, │ │634 }, │ │635 ); │ │636 + _ = try std.fmt.hexToBytes(&conflicting_oids[i], &oid);│ │637 } │ │638 } │ │639 │ ╰────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │640 // │ │641 // check out the events branch and merge the other events branch into it │ │642 + // │ │643 + │ │644 + const conflicting_merge_oid = blk: { │ │645 + var result = try repo.switchDir(io, allocator, .{ .target = .{ .ref = evt.events_ref } }); │ │646 + defer result.deinit(); │ │647 + │ │648 + var merge = try repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = other_events_ref }} } } }, null); │ │649 + defer merge.deinit(); │ │650 + │ │651 + try std.testing.expect(.success == merge.result); │ │652 + │ │653 + break :blk merge.result.success.oid; │ │654 + }; │ │655 + │ │656 // │ │657 + // consume events into the database │ │658 + // │ │659 + │ │660 + var issue_id: [evt.event_id_size]u8 = undefined; │ │661 + _ = try std.fmt.hexToBytes(&issue_id, &events_to_consume[0].id); │ │662 + │ │663 + { │ │664 + try evt.consume(.xit, repo_opts, io, allocator, &repo, evt.events_ref); │ │665 + │ │666 + // consuming the merge again must be a no-op rather than replaying it │ │667 + try evt.consume(.xit, repo_opts, io, allocator, &repo, evt.events_ref); │ │668 + │ │669 + const haxy_moment = try evt.currentMoment(repo_opts, &repo); │ │670 + │ │671 + // the first parent is the events branch, so its version stays live │ │672 + const issue = try readIssue(Repo.DB, repo_opts.hash, haxy_moment, &arena, &issue_id); │ │673 + try std.testing.expectEqualStrings("bug priority-low ui", issue.tags); │ │674 + │ │675 + // the issue is listed as conflicted, with the merge base recorded │ │676 + const conflicts_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Issue.conflicts_key)) orelse return error.NotFound; │ │677 + const conflicts = try Repo.DB.SortedMap(.read_only).init(conflicts_cursor); │ │678 + try std.testing.expectEqual(1, try conflicts.count()); │ │679 + │ │680 + const order_key = evt.orderKeyDesc(issue.created_ts, &issue_id); │ │681 + const conflict_cursor = try conflicts.getCursor(&order_key) orelse return error.NotFound; │ │682 + const conflict = try Repo.DB.HashMap(.read_only).init(conflict_cursor); │ │683 + │ │684 + const base_cursor = try conflict.getCursor(hash.hashInt(repo_opts.hash, evt.base_record_key)) orelse return error.NotFound; │ │685 + const base = try evt.read(evt.Issue, Repo.DB, repo_opts.hash, &arena, try Repo.DB.HashMap(.read_only).init(base_cursor)); │ │686 + try std.testing.expectEqualStrings("bug priority-high ui", base.tags); │ │687 + │ │688 + // tags is the one field both sides changed differently │ │689 + const fields_cursor = try conflict.getCursor(hash.hashInt(repo_opts.hash, evt.conflicted_fields_key)) orelse return error.NotFound; │ │690 + const conflicted_fields = try fields_cursor.readBytesAlloc(arena.allocator(), null); │ │691 + try std.testing.expectEqualStrings("tags", conflicted_fields); │ │692 + │ │693 + // their whole version is recorded, not just the conflicting field │ │694 + const their_cursor = try conflict.getCursor(hash.hashInt(repo_opts.hash, evt.their_record_key)) orelse return error.NotFound; │ │695 + const theirs = try evt.read(evt.Issue, Repo.DB, repo_opts.hash, &arena, try Repo.DB.HashMap(.read_only).init(their_cursor)); │ │696 + try std.testing.expectEqualStrings("bug priority-medium ui", theirs.tags); │ │697 + try std.testing.expectEqualStrings(issue.title, theirs.title); │ │698 + try std.testing.expectEqualStrings(issue.description, theirs.description); │ │699 + │ │700 + // their value is attributed to the commit that set it on their branch │ │701 + const oids_cursor = try conflict.getCursor(hash.hashInt(repo_opts.hash, evt.their_field_to_oid_key)) orelse return error.NotFound; │ │702 + const oids = try Repo.DB.SortedMap(.read_only).init(oids_cursor); │ │703 + const oid_cursor = try oids.getCursor("tags") orelse return error.NotFound; │ │704 + var their_oid: [hash.byteLen(repo_opts.hash)]u8 = undefined; │ │705 + _ = try oid_cursor.readBytes(&their_oid); │ │706 + try std.testing.expectEqualSlices(u8, &conflicting_oids[1], &their_oid); │ │707 + │ │708 + // ours stays attributed to the commit on the events branch │ │709 + try std.testing.expectEqualSlices(u8, &conflicting_oids[0], &try readOid(Repo.DB, repo_opts.hash, haxy_moment, &issue_id, "tags")); │ │710 + │ │711 + // the losing side's tag didn't leak into the tag index │ │712 + const tag_to_issues_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "tag+status->issue-id-set")) orelse return error.NotFound; │ │713 + const tag_to_issues = try Repo.DB.SortedMap(.read_only).init(tag_to_issues_cursor); │ │714 + try std.testing.expect(null != try tag_to_issues.getCursor("priority-low open")); │ │715 + try std.testing.expect(null == try tag_to_issues.getCursor("priority-medium open")); │ │716 + } │ │717 + │ │718 + // │ │719 + // a merge that touches the issue without conflicting leaves the conflict │ │720 + // alone, since only events settle one │ │721 + // │ │722 + │ │723 + { │ │724 + const their_issue = events_to_consume3[1].event.issue orelse return error.NotFound; │ │725 + │ │726 + var json: std.Io.Writer.Allocating = .init(allocator); │ │727 + defer json.deinit(); │ │728 + │ │729 + // the other branch never saw the conflict and rewrites an unrelated │ │730 + // field │ │731 + const event = evt.EventWithId{ .id = events_to_consume3[1].id, .event = .{ .issue = .{ │ │732 + .title = their_issue.title, │ │733 + .description = "Rewritten on a branch that never merged.", │ │734 + .tags = their_issue.tags, │ │735 + } } }; │ │736 + try std.json.Stringify.value(event, .{}, &json.writer); │ │737 + _ = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, other_events_ref); │ │738 + │ │739 + { │ │740 + var result = try repo.switchDir(io, allocator, .{ .target = .{ .ref = evt.events_ref } }); │ │741 + defer result.deinit(); │ │742 + │ │743 + var merge = try repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = other_events_ref }} } } }, null);│ │744 + defer merge.deinit(); │ │745 + │ │746 + try std.testing.expect(.success == merge.result); │ │747 + } │ │748 + │ │749 + try evt.consume(.xit, repo_opts, io, allocator, &repo, evt.events_ref); │ │750 + │ │751 + const haxy_moment = try evt.currentMoment(repo_opts, &repo); │ │752 + │ │753 + // their edit came over, so the merge really did touch the issue │ │754 + const issue = try readIssue(Repo.DB, repo_opts.hash, haxy_moment, &arena, &issue_id); │ │755 + try std.testing.expectEqualStrings("Rewritten on a branch that never merged.", issue.description); │ │756 + │ │757 + // and the conflict is still waiting to be resolved │ │758 + const conflicts_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Issue.conflicts_key)) orelse return error.NotFound; │ │759 + const conflicts = try Repo.DB.SortedMap(.read_only).init(conflicts_cursor); │ │760 + try std.testing.expectEqual(1, try conflicts.count()); │ │761 + } │ │762 + │ │763 + // │ │764 + // any event settles the conflict, even one that changes no conflicting │ │765 + // field │ │766 + // │ │767 + │ │768 + { │ │769 + try evt.Issue.update(.xit, repo_opts, io, allocator, &repo, &issue_id, .{ .status = .closed }); │ │770 + │ │771 + const haxy_moment = try evt.currentMoment(repo_opts, &repo); │ │772 + const conflicts_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Issue.conflicts_key)) orelse return error.NotFound; │ │773 + const conflicts = try Repo.DB.SortedMap(.read_only).init(conflicts_cursor); │ │774 + try std.testing.expectEqual(0, try conflicts.count()); │ │775 + } │ │776 + │ │777 + // │ │778 + // edit the issue, so the merge below has a resolution to preserve │ │779 + // │ │780 + │ │781 + { │ │782 + try evt.Issue.update(.xit, repo_opts, io, allocator, &repo, &issue_id, .{ .fields = .{ │ │783 + .title = "Login form clears password on validation error", │ │784 + .tags = "bug priority-medium ui", │ │785 + .description = "Submitting an invalid email address resets the password field.", │ │786 + } }); │ │787 + } │ │788 + │ │789 + // │ │790 + // merging a branch that still holds the settled conflict must not bring it │ │791 + // back │ │792 + // │ │793 + │ │794 + { │ │795 + const conflicted_issue = events_to_consume3[0].event.issue orelse return error.NotFound; │ │796 + │ │797 + var json: std.Io.Writer.Allocating = .init(allocator); │ │798 + defer json.deinit(); │ │799 + │ │800 + // the other branch changes the record on a commit that predates the │ │801 + // resolution, so it still carries the conflict │ │802 + const event = evt.EventWithId{ .id = events_to_consume3[0].id, .event = .{ .issue = .{ │ │803 + .title = conflicted_issue.title, │ │804 + .description = conflicted_issue.description, │ │805 + .tags = conflicted_issue.tags, │ │806 + .status = .closed, │ │807 + } } }; │ │808 + try std.json.Stringify.value(event, .{}, &json.writer); │ │809 + _ = try repo.commitAtRef(io, allocator, .{ .parent_oids = &.{conflicting_merge_oid}, .message = json.written() }, null, other_events_ref); │ │810 + │ │811 + { │ │812 + var result = try repo.switchDir(io, allocator, .{ .target = .{ .ref = evt.events_ref } }); │ │813 + defer result.deinit(); │ │814 + │ │815 + var merge = try repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = other_events_ref }} } } }, null);│ │816 + defer merge.deinit(); │ │817 + │ │818 + try std.testing.expect(.success == merge.result); │ │819 + } │ │820 + │ │821 + try evt.consume(.xit, repo_opts, io, allocator, &repo, evt.events_ref); │ │822 + │ │823 + const haxy_moment = try evt.currentMoment(repo_opts, &repo); │ │824 + │ │825 + const conflicts_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Issue.conflicts_key)) orelse return error.NotFound; │ │826 + const conflicts = try Repo.DB.SortedMap(.read_only).init(conflicts_cursor); │ │827 + try std.testing.expectEqual(0, try conflicts.count()); │ │828 + │ │829 + // and the resolution stands │ │830 + const issue = try readIssue(Repo.DB, repo_opts.hash, haxy_moment, &arena, &issue_id); │ │831 + try std.testing.expectEqualStrings("bug priority-medium ui", issue.tags); │ │832 + } │ │833 + │ │834 + // │ │835 + // diverge again, with each side editing different fields of one issue and │ │836 + // only the other side touching a second issue │ │837 + // │ │838 + │ │839 + const second_issue = events_to_consume[1].event.issue orelse return error.NotFound; │ │840 + const third_issue = events_to_consume[2].event.issue orelse return error.NotFound; │ │841 │ │842 + var retitle_oid: [hash.byteLen(repo_opts.hash)]u8 = undefined; │ │843 + var rewrite_oid: [hash.byteLen(repo_opts.hash)]u8 = undefined; │ │844 + │ │845 { │ │846 + const base_oid = try repo.readRef(io, evt.events_ref) orelse return error.NotFound; │ │847 + │ │848 + var json: std.Io.Writer.Allocating = .init(allocator); │ │849 + defer json.deinit(); │ │850 + │ │851 + // the events branch only retitles the second issue │ │852 + { │ │853 + const event = evt.EventWithId{ .id = events_to_consume[1].id, .event = .{ .issue = .{ │ │854 + .title = "Search results ignore the archived project filter", │ │855 + .description = second_issue.description, │ │856 + .tags = second_issue.tags, │ │857 + } } }; │ │858 + try std.json.Stringify.value(event, .{}, &json.writer); │ │859 + const oid = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, evt.events_ref); │ │860 + _ = try std.fmt.hexToBytes(&retitle_oid, &oid); │ │861 + } │ │862 + │ │863 + // the other branch rewrites the second issue's description and drops one │ │864 + // of its tags, then retags the third issue │ │865 + { │ │866 + json.clearRetainingCapacity(); │ │867 + const event = evt.EventWithId{ .id = events_to_consume[1].id, .event = .{ .issue = .{ │ │868 + .title = second_issue.title, │ │869 + .description = "Archived projects are ranked before the flag is applied.", │ │870 + .tags = "bug search", │ │871 + } } }; │ │872 + try std.json.Stringify.value(event, .{}, &json.writer); │ │873 + const oid = try repo.commitAtRef(io, allocator, .{ .parent_oids = &.{base_oid}, .message = json.written() }, null, other_events_ref); │ │874 + _ = try std.fmt.hexToBytes(&rewrite_oid, &oid); │ │875 + } │ │876 + { │ │877 + json.clearRetainingCapacity(); │ │878 + const event = evt.EventWithId{ .id = events_to_consume[2].id, .event = .{ .issue = .{ │ │879 + .title = third_issue.title, │ │880 + .description = third_issue.description, │ │881 + .tags = "enhancement roadmap", │ │882 + } } }; │ │883 + try std.json.Stringify.value(event, .{}, &json.writer); │ │884 + _ = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, other_events_ref); │ │885 + } │ │886 + } │ │887 + │ │888 + { │ │889 var result = try repo.switchDir(io, allocator, .{ .target = .{ .ref = evt.events_ref } }); │ │890 defer result.deinit(); │ │891 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 895 try std.testing.expect(.success == merge.result); │ │ 896 } │ │ 897 │ │ 898 + { │ │ 899 + try evt.consume(.xit, repo_opts, io, allocator, &repo, evt.events_ref); │ │ 900 + │ │ 901 + const haxy_moment = try evt.currentMoment(repo_opts, &repo); │ │ 902 + │ │ 903 + var second_id: [evt.event_id_size]u8 = undefined; │ │ 904 + _ = try std.fmt.hexToBytes(&second_id, &events_to_consume[1].id); │ │ 905 + │ │ 906 + // each side's edit to a different field survives, without conflicting │ │ 907 + const merged = try readIssue(Repo.DB, repo_opts.hash, haxy_moment, &arena, &second_id); │ │ 908 + try std.testing.expectEqualStrings("Search results ignore the archived project filter", merged.title); │ │ 909 + try std.testing.expectEqualStrings("Archived projects are ranked before the flag is applied.", merged.description); │ │ 910 + try std.testing.expectEqualStrings("bug search", merged.tags); │ │ 911 + │ │ 912 + // each merged field is attributed to the branch it came from │ │ 913 + try std.testing.expectEqualSlices(u8, &retitle_oid, &try readOid(Repo.DB, repo_opts.hash, haxy_moment, &second_id, "title")); │ │ 914 + try std.testing.expectEqualSlices(u8, &rewrite_oid, &try readOid(Repo.DB, repo_opts.hash, haxy_moment, &second_id, "description")); │ │ 915 + │ │ 916 + const conflicts_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Issue.conflicts_key)) orelse return error.NotFound; │ │ 917 + const conflicts = try Repo.DB.SortedMap(.read_only).init(conflicts_cursor); │ │ 918 + try std.testing.expectEqual(0, try conflicts.count()); │ │ 919 + │ │ 920 + // tags the merged-in side dropped don't survive in the tag index, │ │ 921 + // whether we edited that issue or left it alone │ │ 922 + const tag_to_issues_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "tag+status->issue-id-set")) orelse return error.NotFound; │ │ 923 + const tag_to_issues = try Repo.DB.SortedMap(.read_only).init(tag_to_issues_cursor); │ │ 924 + │ │ 925 + var third_id: [evt.event_id_size]u8 = undefined; │ │ 926 + _ = try std.fmt.hexToBytes(&third_id, &events_to_consume[2].id); │ │ 927 + const retagged = try readIssue(Repo.DB, repo_opts.hash, haxy_moment, &arena, &third_id); │ │ 928 + try std.testing.expectEqualStrings("enhancement roadmap", retagged.tags); │ │ 929 + │ │ 930 + try std.testing.expect(null == try tag_to_issues.getCursor("backend open")); │ │ 931 + try std.testing.expect(null == try tag_to_issues.getCursor("preferences open")); │ │ 932 + try std.testing.expect(null != try tag_to_issues.getCursor("roadmap open")); │ │ 933 + try std.testing.expect(null != try tag_to_issues.getCursor("enhancement open")); │ │ 934 + } │ │ 935 + │ │ 936 // │ │ 652 - // consume events into the database │ │ 937 + // a deletion on the other branch carries over, unless we edited the issue, │ │ 938 + // in which case the edit wins │ │ 939 // │ │ 940 │ │ 655 - try std.testing.expectError(error.MergeConflict, evt.consume(.xit, repo_opts, io, allocator, &repo, evt.events_ref)); │ │ 941 + { │ │ 942 + const edited_issue = events_to_consume2[1].event.issue orelse return error.NotFound; │ │ 943 + │ │ 944 + const base_oid = try repo.readRef(io, evt.events_ref) orelse return error.NotFound; │ │ 945 + │ │ 946 + var json: std.Io.Writer.Allocating = .init(allocator); │ │ 947 + defer json.deinit(); │ │ 948 + │ │ 949 + // we retitle one of the two the other branch is about to drop │ │ 950 + { │ │ 951 + const event = evt.EventWithId{ .id = events_to_consume2[1].id, .event = .{ .issue = .{ │ │ 952 + .title = "Kept by the edit", │ │ 953 + .description = edited_issue.description, │ │ 954 + .tags = edited_issue.tags, │ │ 955 + } } }; │ │ 956 + try std.json.Stringify.value(event, .{}, &json.writer); │ │ 957 + _ = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, evt.events_ref); │ │ 958 + } │ │ 959 + │ │ 960 + // the other branch deletes both │ │ 961 + { │ │ 962 + json.clearRetainingCapacity(); │ │ 963 + const event = evt.EventWithId{ .id = events_to_consume2[0].id, .event = .{ .issue = null } }; │ │ 964 + try std.json.Stringify.value(event, .{}, &json.writer); │ │ 965 + _ = try repo.commitAtRef(io, allocator, .{ .parent_oids = &.{base_oid}, .message = json.written() }, null, other_events_ref); │ │ 966 + } │ │ 967 + { │ │ 968 + json.clearRetainingCapacity(); │ │ 969 + const event = evt.EventWithId{ .id = events_to_consume2[1].id, .event = .{ .issue = null } }; │ │ 970 + try std.json.Stringify.value(event, .{}, &json.writer); │ │ 971 + _ = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, other_events_ref); │ │ 972 + } │ │ 973 + │ │ 974 + { │ │ 975 + var result = try repo.switchDir(io, allocator, .{ .target = .{ .ref = evt.events_ref } }); │ │ 976 + defer result.deinit(); │ │ 977 + │ │ 978 + var merge = try repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = other_events_ref }} } } }, null); │ │ 979 + defer merge.deinit(); │ │ 980 + │ │ 981 + try std.testing.expect(.success == merge.result); │ │ 982 + } │ │ 983 + │ │ 984 + try evt.consume(.xit, repo_opts, io, allocator, &repo, evt.events_ref); │ │ 985 + │ │ 986 + const haxy_moment = try evt.currentMoment(repo_opts, &repo); │ │ 987 + │ │ 988 + // the one we left alone is gone, and its tags left the index with it │ │ 989 + var dropped_id: [evt.event_id_size]u8 = undefined; │ │ 990 + _ = try std.fmt.hexToBytes(&dropped_id, &events_to_consume2[0].id); │ │ 991 + try std.testing.expectError(error.NotFound, readIssue(Repo.DB, repo_opts.hash, haxy_moment, &arena, &dropped_id)); │ │ 992 + │ │ 993 + const tag_to_issues_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "tag+status->issue-id-set")) orelse return error.NotFound; │ │ 994 + const tag_to_issues = try Repo.DB.SortedMap(.read_only).init(tag_to_issues_cursor); │ │ 995 + try std.testing.expect(null == try tag_to_issues.getCursor("kanban open")); │ │ 996 + │ │ 997 + // the one we edited survives, with our edit │ │ 998 + var kept_id: [evt.event_id_size]u8 = undefined; │ │ 999 + _ = try std.fmt.hexToBytes(&kept_id, &events_to_consume2[1].id); │ │1000 + const kept = try readIssue(Repo.DB, repo_opts.hash, haxy_moment, &arena, &kept_id); │ │1001 + try std.testing.expectEqualStrings("Kept by the edit", kept.title); │ │1002 + } │ │1003 +} │ │1004 + │ │1005 +// the commit that last set `field_name` on the issue `id` names │ │1006 +fn readOid( │ │1007 + comptime DB: type, │ │1008 + comptime hash_kind: hash.HashKind, │ │1009 + haxy_moment: DB.HashMap(.read_only), │ │1010 + id: *const [evt.event_id_size]u8, │ │1011 + comptime field_name: []const u8, │ │1012 +) ![hash.byteLen(hash_kind)]u8 { │ │1013 + const id_to_field_to_oid_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, evt.Issue.id_to_field_to_oid_key)) orelse return error.NotFound;│ │1014 + const id_to_field_to_oid = try DB.HashMap(.read_only).init(id_to_field_to_oid_cursor); │ │1015 + const field_oids_cursor = try id_to_field_to_oid.getCursor(hash.hashInt(hash_kind, id)) orelse return error.NotFound; │ │1016 + const field_oids = try DB.SortedMap(.read_only).init(field_oids_cursor); │ │1017 + const cursor = try field_oids.getCursor(field_name) orelse return error.NotFound; │ │1018 + var oid: [hash.byteLen(hash_kind)]u8 = undefined; │ │1019 + _ = try cursor.readBytes(&oid); │ │1020 + return oid; │ │1021 +} │ │1022 + │ │1023 +// the issue `id` names, read out of `event-id->issue` │ │1024 +fn readIssue( │ │1025 + comptime DB: type, │ │1026 + comptime hash_kind: hash.HashKind, │ │1027 + haxy_moment: DB.HashMap(.read_only), │ │1028 + arena: *std.heap.ArenaAllocator, │ │1029 + id: *const [evt.event_id_size]u8, │ │1030 +) !evt.Issue { │ │1031 + const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, "event-id->issue")) orelse return error.NotFound; │ │1032 + const event_id_to_issue = try DB.HashMap(.read_only).init(event_id_to_issue_cursor); │ │1033 + const issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(hash_kind, id)) orelse return error.NotFound; │ │1034 + const issue_map = try DB.HashMap(.read_only).init(issue_cursor); │ │1035 + return try evt.read(evt.Issue, DB, hash_kind, arena, issue_map); │ │1036 } │ │1037 │ │1038 test "user and repo" { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────╮ │1154 │ │1155 try std.testing.expectEqual(1, try user_repos.count()); │ │1156 │ │ 777 - // the set is keyed by orderKey([created-ts][event-id]) │ │ 778 - const order_key = evt.orderKey(repo_event.created_ts, &repo_event_id); │ │1157 + // the set is keyed by orderKeyDesc([created-ts][event-id]) │ │1158 + const order_key = evt.orderKeyDesc(repo_event.created_ts, &repo_event_id);│ │1159 try std.testing.expect(try user_repos.contains(&order_key)); │ │1160 } │ │1161 │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────╮ │1218 } │ │1219 } │ │1220 │ │ 841 -test "repos and users paginate in creation order" {│ │1221 +test "repos and users paginate newest first" { │ │1222 const io = std.testing.io; │ │1223 const allocator = std.testing.allocator; │ │1224 const temp_dir_name = "temp-event-order"; │ ╰─────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1267 try std.testing.expectEqual(expected.len, try set.count()); │ │1268 for (expected, 0..) |id, i| { │ │1269 const kv = (try set.getIndexKeyValuePair(@intCast(i))) orelse return error.NotFound;│ │ 890 - // the key is orderKey ([timestamp][event-id]); its trailing bytes are the id │ │1270 + // the key is orderKeyDesc ([timestamp][event-id]); its trailing bytes are the id │ │1271 var order_key: [@sizeOf(u64) + evt.event_id_size]u8 = undefined; │ │1272 _ = try kv.key_cursor.readBytes(&order_key); │ │1273 try std.testing.expectEqualSlices(u8, id, order_key[@sizeOf(u64)..]); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1288 │ │1289 { │ │1290 const moment = try evt.currentMoment(repo_opts, &repo); │ │ 911 - try Check.order(Repo.DB, repo_opts.hash, moment, &.{ &repo_ids[0], &repo_ids[1], &repo_ids[2], &repo_ids[3] });│ │1291 + try Check.order(Repo.DB, repo_opts.hash, moment, &.{ &repo_ids[3], &repo_ids[2], &repo_ids[1], &repo_ids[0] });│ │1292 │ │1293 // the single user shows up in its own ordered set │ │1294 const ucur = try moment.getCursor(hash.hashInt(repo_opts.hash, "user-id-set")) orelse return error.NotFound; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1302 }); │ │1303 { │ │1304 const moment = try evt.currentMoment(repo_opts, &repo); │ │ 925 - try Check.order(Repo.DB, repo_opts.hash, moment, &.{ &repo_ids[0], &repo_ids[2], &repo_ids[3] }); │ │1305 + try Check.order(Repo.DB, repo_opts.hash, moment, &.{ &repo_ids[3], &repo_ids[2], &repo_ids[0] }); │ │1306 } │ │1307 │ │ 928 - // update repo0 at a later timestamp -> keeps its original slot │ │1308 + // update repo0 at a later timestamp -> keeps its original place │ │1309 try evt.commitAndConsume(.xit, repo_opts, io, allocator, &repo, evt.events_ref, &[_]evt.EventWithId{ │ │1310 .{ .id = std.fmt.bytesToHex(repo_ids[0], .lower), .timestamp = 300, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo0", .description = "updated" } } },│ │1311 }); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯