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