╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ──────────╮ │src/event/Issue.zig│ ╰───────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │147 - } else { │ │148 - // drop it from the ordered sets using its recorded creation timestamp and status │ │149 - if (try event_id_to_issue.getCursor(issue_key)) |existing_cursor| { │ │150 - const existing_issue = try DB.HashMap(.read_only).init(existing_cursor); │ │151 - const existing_record = try evt.read(Record, DB, hash_kind, arena, existing_issue); │ │152 - const order_key = evt.orderKeyDesc(existing_record.created_ts, event_id); │ │153 - │ │154 - const status_set = try statusSet(DB, status_to_issues, existing_record.event.status); │ │155 - _ = try status_set.remove(&order_key); │ │156 - │ │157 - try removeFromTagSets(DB, tag_to_issues, existing_record.event.tags, existing_record.event.status, &order_key);│ │158 - │ │159 - const issue_id_set_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, id_set_key)); │ │160 - const issue_id_set = try DB.SortedSet(.read_write).init(issue_id_set_cursor); │ │161 - _ = try issue_id_set.remove(&order_key); │ │162 - │ │163 - _ = try conflicts.remove(&order_key); │ │164 - _ = try issue_id_to_field_to_oid.remove(issue_key); │ │165 - } │ │166 - if (!try event_id_to_issue.remove(issue_key)) return error.EventNotFound; │ │157 } │ │158 } │ │159 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────╮ │src/event/Repo.zig│ ╰──────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │10 // what the db stores: the event's data plus the commit-derived fields │ │11 pub const Record = struct { │ │12 event: Self, │ │13 + deleted: bool = false, │ │14 created_ts: u64 = 0, // the commit timestamp of the event that first created this repo│ │15 │ │16 // a repo's key in the name index. it's unique per owner, so the read side │ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 67 const repo_id_to_field_to_oid_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, id_to_field_to_oid_key)); │ │ 68 const repo_id_to_field_to_oid = try DB.HashMap(.read_write).init(repo_id_to_field_to_oid_cursor); │ │ 69 │ │ 69 - if (record_maybe) |record| { │ │ 70 - try validateName(record.event.name); │ │ 70 + var existing_record_maybe: ?Record = null; │ │ 71 + const existing_cursor_maybe = try event_id_to_repo.getCursor(repo_key); │ │ 72 + if (existing_cursor_maybe) |existing_cursor| { │ │ 73 + const existing_repo = try DB.HashMap(.read_only).init(existing_cursor); │ │ 74 + existing_record_maybe = try evt.read(Record, DB, hash_kind, arena, existing_repo); │ │ 75 + } │ │ 76 │ │ 72 - var record_to_write = record; │ │ 77 + var record_to_write = if (record_maybe) |record| │ │ 78 + record │ │ 79 + else blk: { │ │ 80 + var record = existing_record_maybe orelse return error.EventNotFound; │ │ 81 + record.deleted = true; │ │ 82 + break :blk record; │ │ 83 + }; │ │ 84 │ │ 74 - const repo_path = try record.indexKey(arena.allocator()); │ │ 85 + if (!record_to_write.deleted) try validateName(record_to_write.event.name); │ │ 86 │ │ 76 - const user_id_to_repo_id_set_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "user-id->repo-id-set")); │ │ 77 - const user_id_to_repo_id_set = try DB.HashMap(.read_write).init(user_id_to_repo_id_set_cursor); │ │ 87 + const user_id_to_repo_id_set_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "user-id->repo-id-set")); │ │ 88 + const user_id_to_repo_id_set = try DB.HashMap(.read_write).init(user_id_to_repo_id_set_cursor); │ │ 89 │ │ 79 - // if this event_id already maps to a repo under a different key (its │ │ 80 - // name or owner changed), drop the stale index entries first │ │ 81 - var existing_record_maybe: ?Record = null; │ │ 82 - const existing_cursor_maybe = try event_id_to_repo.getCursor(repo_key); │ │ 83 - if (existing_cursor_maybe) |existing_cursor| { │ │ 84 - const existing_repo = try DB.HashMap(.read_only).init(existing_cursor); │ │ 85 - const existing_record = try evt.read(Record, DB, hash_kind, arena, existing_repo); │ │ 86 - existing_record_maybe = existing_record; │ │ 87 - // updates preserve the original creation timestamp │ │ 88 - record_to_write.created_ts = existing_record.created_ts; │ │ 90 + if (existing_record_maybe) |existing_record| { │ │ 91 + // updates preserve the original creation timestamp │ │ 92 + record_to_write.created_ts = existing_record.created_ts; │ │ 93 + const order_key = evt.orderKeyDesc(existing_record.created_ts, event_id); │ │ 94 │ │ 90 - // any event settles the conflict, since resolving in the ui may │ │ 91 - // keep our own values and so change nothing to detect │ │ 92 - if (event_oid != null) { │ │ 93 - _ = try conflicts.remove(&evt.orderKeyDesc(existing_record.created_ts, event_id)); │ │ 94 - } │ │ 95 + // drop the old active indexes; active values are re-added below │ │ 96 + if (!existing_record.deleted) { │ │ 97 const existing_path = try existing_record.indexKey(arena.allocator()); │ │ 96 - if (!std.mem.eql(u8, existing_path, repo_path)) { │ │ 97 - _ = try name_to_repo_id.remove(hash.hashInt(hash_kind, existing_path)); │ │ 98 - } │ │ 99 - if (!std.mem.eql(u8, existing_record.event.user_id, record.event.user_id)) { │ │100 - const old_user_repos_cursor = try user_id_to_repo_id_set.putCursor(hash.hashInt(hash_kind, existing_record.event.user_id)); │ │101 - const old_user_repos = try DB.SortedSet(.read_write).init(old_user_repos_cursor); │ │102 - const order_key = evt.orderKeyDesc(existing_record.created_ts, event_id); │ │103 - _ = try old_user_repos.remove(&order_key); │ │104 - } │ │105 - } │ │ 98 + _ = try name_to_repo_id.remove(hash.hashInt(hash_kind, existing_path)); │ │ 99 │ │107 - try evt.writeOid(Self, DB, hash_kind, repo_id_to_field_to_oid, repo_key, if (existing_record_maybe) |existing| existing.event else null, record.event, event_oid); │ │108 - │ │109 - const repo_cursor = try event_id_to_repo.putCursor(repo_key); │ │110 - const repo = try DB.HashMap(.read_write).init(repo_cursor); │ │111 - try evt.upsert(Record, DB, hash_kind, repo, record_to_write); │ │112 - │ │113 - try name_to_repo_id.put(hash.hashInt(hash_kind, repo_path), .{ .bytes = event_id }); │ │100 + const old_user_repos_cursor = try user_id_to_repo_id_set.putCursor(hash.hashInt(hash_kind, existing_record.event.user_id)); │ │101 + const old_user_repos = try DB.SortedSet(.read_write).init(old_user_repos_cursor); │ │102 + _ = try old_user_repos.remove(&order_key); │ │103 + } │ │104 │ │115 - // first time we've seen this repo: add it to the ordered set the repos │ │116 - // view paginates through. the key embeds the event id, so the set needs │ │117 - // no value. │ │118 - if (existing_cursor_maybe == null) { │ │119 - const repo_id_set_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "repo-id-set")); │ │120 - const repo_id_set = try DB.SortedSet(.read_write).init(repo_id_set_cursor); │ │121 - const order_key = evt.orderKeyDesc(record_to_write.created_ts, event_id); │ │122 - try repo_id_set.put(&order_key); │ │105 + // any event settles the conflict, since resolving in the ui may │ │106 + // keep our own values and so change nothing to detect │ │107 + if (event_oid != null or record_to_write.deleted) { │ │108 + _ = try conflicts.remove(&order_key); │ │109 } │ │110 + } │ │111 │ │125 - // each user's repos are a set ordered by creation time (newest first), so │ │126 - // the user page paginates them the same way the home repos list does. │ │127 - const user_repos_cursor = try user_id_to_repo_id_set.putCursor(hash.hashInt(hash_kind, record.event.user_id)); │ │128 - const user_repos = try DB.SortedSet(.read_write).init(user_repos_cursor); │ │129 - const user_repo_order_key = evt.orderKeyDesc(record_to_write.created_ts, event_id); │ │130 - try user_repos.put(&user_repo_order_key); │ │131 - } else { │ │132 - if (try event_id_to_repo.getCursor(repo_key)) |existing_repo_cursor| { │ │133 - const existing_repo = try DB.HashMap(.read_only).init(existing_repo_cursor); │ │134 - const existing_repo_record = try evt.read(Record, DB, hash_kind, arena, existing_repo); │ │112 + try evt.writeOid(Self, DB, hash_kind, repo_id_to_field_to_oid, repo_key, if (existing_record_maybe) |existing| existing.event else null, record_to_write.event, event_oid);│ │113 │ │136 - const existing_path = try existing_repo_record.indexKey(arena.allocator()); │ │137 - _ = try name_to_repo_id.remove(hash.hashInt(hash_kind, existing_path)); │ │114 + const repo_cursor = try event_id_to_repo.putCursor(repo_key); │ │115 + const repo = try DB.HashMap(.read_write).init(repo_cursor); │ │116 + try evt.upsert(Record, DB, hash_kind, repo, record_to_write); │ │117 │ │139 - // drop it from the ordered set using its recorded creation timestamp │ │140 - const repo_id_set_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "repo-id-set")); │ │141 - const repo_id_set = try DB.SortedSet(.read_write).init(repo_id_set_cursor); │ │142 - const order_key = evt.orderKeyDesc(existing_repo_record.created_ts, event_id); │ │143 - _ = try repo_id_set.remove(&order_key); │ │118 + const order_key = evt.orderKeyDesc(record_to_write.created_ts, event_id); │ │119 │ │145 - const user_id_to_repo_id_set_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "user-id->repo-id-set")); │ │146 - const user_id_to_repo_id_set = try DB.HashMap(.read_write).init(user_id_to_repo_id_set_cursor); │ │120 + // the id set retains tombstones so merges can carry deletions │ │121 + if (existing_cursor_maybe == null) { │ │122 + const repo_id_set_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, id_set_key)); │ │123 + const repo_id_set = try DB.SortedSet(.read_write).init(repo_id_set_cursor); │ │124 + try repo_id_set.put(&order_key); │ │125 + } │ │126 │ │148 - const user_key = hash.hashInt(hash_kind, existing_repo_record.event.user_id); │ │149 - const user_repos_cursor = try user_id_to_repo_id_set.putCursor(user_key); │ │150 - const user_repos = try DB.SortedSet(.read_write).init(user_repos_cursor); │ │151 - _ = try user_repos.remove(&order_key); │ │127 + if (!record_to_write.deleted) { │ │128 + const repo_path = try record_to_write.indexKey(arena.allocator()); │ │129 + try name_to_repo_id.put(hash.hashInt(hash_kind, repo_path), .{ .bytes = event_id }); │ │130 │ │153 - _ = try conflicts.remove(&order_key); │ │154 - _ = try repo_id_to_field_to_oid.remove(repo_key); │ │155 - } │ │156 - if (!try event_id_to_repo.remove(repo_key)) return error.EventNotFound; │ │131 + // each user's repos are ordered by creation time, newest first │ │132 + const user_repos_cursor = try user_id_to_repo_id_set.putCursor(hash.hashInt(hash_kind, record_to_write.event.user_id)); │ │133 + const user_repos = try DB.SortedSet(.read_write).init(user_repos_cursor); │ │134 + try user_repos.put(&order_key); │ │135 } │ │136 } │ │137 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────╮ │src/event/User.zig│ ╰──────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │15 // what the db stores: the event's data plus the commit-derived fields │ │16 pub const Record = struct { │ │17 event: Self, │ │18 + deleted: bool = false, │ │19 created_ts: u64 = 0, // the commit timestamp of the event that first created this user│ │20 │ │21 // a user's key in the name index │ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 90 const user_id_to_field_to_oid_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, id_to_field_to_oid_key)); │ │ 91 const user_id_to_field_to_oid = try DB.HashMap(.read_write).init(user_id_to_field_to_oid_cursor); │ │ 92 │ │ 92 - if (record_maybe) |record| { │ │ 93 - try validateName(record.event.name); │ │ 93 + var existing_record_maybe: ?Record = null; │ │ 94 + const existing_cursor_maybe = try event_id_to_user.getCursor(user_key); │ │ 95 + if (existing_cursor_maybe) |existing_cursor| { │ │ 96 + const existing_user = try DB.HashMap(.read_only).init(existing_cursor); │ │ 97 + existing_record_maybe = try evt.read(Record, DB, hash_kind, arena, existing_user); │ │ 98 + } │ │ 99 │ │ 95 - var record_to_write = record; │ │100 + var record_to_write = if (record_maybe) |record| │ │101 + record │ │102 + else blk: { │ │103 + var record = existing_record_maybe orelse return error.EventNotFound; │ │104 + record.deleted = true; │ │105 + break :blk record; │ │106 + }; │ │107 │ │ 97 - // if this event_id already maps to a user with a different name or │ │ 98 - // email, drop the stale index entries first │ │ 99 - var existing_record_maybe: ?Record = null; │ │100 - const existing_cursor_maybe = try event_id_to_user.getCursor(user_key); │ │101 - if (existing_cursor_maybe) |existing_cursor| { │ │102 - const existing_user = try DB.HashMap(.read_only).init(existing_cursor); │ │103 - const existing_record = try evt.read(Record, DB, hash_kind, arena, existing_user); │ │104 - existing_record_maybe = existing_record; │ │105 - // updates preserve the original creation timestamp │ │106 - record_to_write.created_ts = existing_record.created_ts; │ │107 - if (!std.mem.eql(u8, existing_record.event.name, record.event.name)) { │ │108 - _ = try name_to_user_id.remove(hash.hashInt(hash_kind, existing_record.event.name)); │ │109 - } │ │110 - if (!std.mem.eql(u8, existing_record.event.email, record.event.email)) { │ │111 - _ = try email_to_user_id.remove(hash.hashInt(hash_kind, existing_record.event.email)); │ │112 - } │ │108 + if (!record_to_write.deleted) try validateName(record_to_write.event.name); │ │109 │ │114 - // any event settles the conflict, since resolving in the ui may │ │115 - // keep our own values and so change nothing to detect │ │116 - if (event_oid != null) { │ │117 - _ = try conflicts.remove(&evt.orderKeyDesc(existing_record.created_ts, event_id)); │ │118 - } │ │110 + if (existing_record_maybe) |existing_record| { │ │111 + // updates preserve the original creation timestamp │ │112 + record_to_write.created_ts = existing_record.created_ts; │ │113 + const order_key = evt.orderKeyDesc(existing_record.created_ts, event_id); │ │114 + │ │115 + // drop the old active indexes; active values are re-added below │ │116 + if (!existing_record.deleted) { │ │117 + _ = try name_to_user_id.remove(hash.hashInt(hash_kind, existing_record.event.name)); │ │118 + _ = try email_to_user_id.remove(hash.hashInt(hash_kind, existing_record.event.email)); │ │119 } │ │120 │ │121 - try evt.writeOid(Self, DB, hash_kind, user_id_to_field_to_oid, user_key, if (existing_record_maybe) |existing| existing.event else null, record.event, event_oid); │ │121 + // any event settles the conflict, since resolving in the ui may │ │122 + // keep our own values and so change nothing to detect │ │123 + if (event_oid != null or record_to_write.deleted) { │ │124 + _ = try conflicts.remove(&order_key); │ │125 + } │ │126 + } │ │127 │ │123 - const user_cursor = try event_id_to_user.putCursor(user_key); │ │124 - const user = try DB.HashMap(.read_write).init(user_cursor); │ │125 - try evt.upsert(Record, DB, hash_kind, user, record_to_write); │ │128 + try evt.writeOid(Self, DB, hash_kind, user_id_to_field_to_oid, user_key, if (existing_record_maybe) |existing| existing.event else null, record_to_write.event, event_oid);│ │129 │ │127 - try name_to_user_id.put(hash.hashInt(hash_kind, record.event.name), .{ .bytes = event_id }); │ │128 - try email_to_user_id.put(hash.hashInt(hash_kind, record.event.email), .{ .bytes = event_id }); │ │130 + const user_cursor = try event_id_to_user.putCursor(user_key); │ │131 + const user = try DB.HashMap(.read_write).init(user_cursor); │ │132 + try evt.upsert(Record, DB, hash_kind, user, record_to_write); │ │133 │ │130 - // first time we've seen this user: add it to the ordered set the users │ │131 - // view paginates through. the key embeds the event id, so the set needs │ │132 - // no value. │ │133 - if (existing_cursor_maybe == null) { │ │134 - const user_id_set_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "user-id-set")); │ │135 - const user_id_set = try DB.SortedSet(.read_write).init(user_id_set_cursor); │ │136 - const order_key = evt.orderKeyDesc(record_to_write.created_ts, event_id); │ │137 - try user_id_set.put(&order_key); │ │138 - } │ │139 - } else { │ │140 - // read the user so we can drop its name and email index entries │ │141 - if (try event_id_to_user.getCursor(user_key)) |existing_cursor| { │ │142 - const existing_user = try DB.HashMap(.read_only).init(existing_cursor); │ │143 - const existing_record = try evt.read(Record, DB, hash_kind, arena, existing_user); │ │144 - _ = try name_to_user_id.remove(hash.hashInt(hash_kind, existing_record.event.name)); │ │145 - _ = try email_to_user_id.remove(hash.hashInt(hash_kind, existing_record.event.email)); │ │134 + const order_key = evt.orderKeyDesc(record_to_write.created_ts, event_id); │ │135 │ │147 - // drop it from the ordered set using its recorded creation timestamp │ │148 - const user_id_set_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "user-id-set")); │ │149 - const user_id_set = try DB.SortedSet(.read_write).init(user_id_set_cursor); │ │150 - const order_key = evt.orderKeyDesc(existing_record.created_ts, event_id); │ │151 - _ = try user_id_set.remove(&order_key); │ │152 - │ │153 - _ = try conflicts.remove(&order_key); │ │154 - _ = try user_id_to_field_to_oid.remove(user_key); │ │155 - } │ │136 + // the id set retains tombstones so merges can carry deletions │ │137 + if (existing_cursor_maybe == null) { │ │138 + const user_id_set_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, id_set_key)); │ │139 + const user_id_set = try DB.SortedSet(.read_write).init(user_id_set_cursor); │ │140 + try user_id_set.put(&order_key); │ │141 + } │ │142 │ │157 - if (!try event_id_to_user.remove(user_key)) return error.EventNotFound; │ │143 + if (!record_to_write.deleted) { │ │144 + try name_to_user_id.put(hash.hashInt(hash_kind, record_to_write.event.name), .{ .bytes = event_id }); │ │145 + try email_to_user_id.put(hash.hashInt(hash_kind, record_to_write.event.email), .{ .bytes = event_id }); │ │146 + } │ │147 │ │148 + const became_deleted = record_to_write.deleted and if (existing_record_maybe) |existing| !existing.deleted else true; │ │149 + if (became_deleted) { │ │150 // the user's repos go with them: the repo name index is keyed by owner │ │151 // id, so one left behind is listed but can never be resolved again │ │152 const user_id_to_repo_id_set_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "user-id->repo-id-set")); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────╮ │src/test/event.zig│ ╰──────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1000 │ │1001 const haxy_moment = try evt.currentMoment(repo_opts, &repo); │ │1002 │ │1003 - // the one we left alone is gone, and its tags left the index with it │ │1003 + // the one we left alone is tombstoned, and its tags left the index │ │1004 var dropped_id: [evt.event_id_size]u8 = undefined; │ │1005 _ = try std.fmt.hexToBytes(&dropped_id, &events_to_consume2[0].id); │ │1006 - try std.testing.expectError(error.NotFound, readIssue(Repo.DB, repo_opts.hash, haxy_moment, &arena, &dropped_id)); │ │1006 + const dropped = try readIssue(Repo.DB, repo_opts.hash, haxy_moment, &arena, &dropped_id); │ │1007 + try std.testing.expect(dropped.deleted); │ │1008 │ │1009 const tag_to_issues_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "tag+status->issue-id-set")) orelse return error.NotFound;│ │1010 const tag_to_issues = try Repo.DB.SortedMap(.read_only).init(tag_to_issues_cursor); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────╮ │1014 var kept_id: [evt.event_id_size]u8 = undefined; │ │1015 _ = try std.fmt.hexToBytes(&kept_id, &events_to_consume2[1].id); │ │1016 const kept = try readIssue(Repo.DB, repo_opts.hash, haxy_moment, &arena, &kept_id);│ │1017 + try std.testing.expect(!kept.deleted); │ │1018 try std.testing.expectEqualStrings("Kept by the edit", kept.event.title); │ │1019 } │ │1020 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1201 const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; │ │1202 const event_id_to_repo = try Repo.DB.HashMap(.read_only).init(event_id_to_repo_cursor); │ │1203 │ │1202 - try std.testing.expect(null == try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id))); │ │1204 + const repo_cursor = try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id)) orelse return error.NotFound; │ │1205 + const repo_map = try Repo.DB.HashMap(.read_only).init(repo_cursor); │ │1206 + const repo_event = try evt.read(evt.Repo.Record, Repo.DB, repo_opts.hash, &arena, repo_map); │ │1207 + try std.testing.expect(repo_event.deleted); │ │1208 │ │1209 // get the repos created by the user │ │1210 const user_id_to_repo_id_set_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "user-id->repo-id-set")) orelse return error.NotFound;│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1217 try std.testing.expectEqual(0, try user_repos.count()); │ │1218 } │ │1219 │ │1220 + // a non-null payload restores the repo and its indexes │ │1221 + try evt.consume(.xit, repo_opts, io, allocator, &repo, evt.events_ref, &[_]evt.EventWithId{.{ │ │1222 + .id = std.fmt.bytesToHex(repo_event_id, .lower), │ │1223 + .author_email = author_email, │ │1224 + .event = events_to_consume[2].event, │ │1225 + }}); │ │1226 + │ │1227 + { │ │1228 + const haxy_moment = try evt.currentMoment(repo_opts, &repo); │ │1229 + const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; │ │1230 + const event_id_to_repo = try Repo.DB.HashMap(.read_only).init(event_id_to_repo_cursor); │ │1231 + const repo_cursor = try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id)) orelse return error.NotFound; │ │1232 + const repo_map = try Repo.DB.HashMap(.read_only).init(repo_cursor); │ │1233 + const repo_event = try evt.read(evt.Repo.Record, Repo.DB, repo_opts.hash, &arena, repo_map); │ │1234 + try std.testing.expect(!repo_event.deleted); │ │1235 + │ │1236 + const user_id_to_repo_id_set_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "user-id->repo-id-set")) orelse return error.NotFound;│ │1237 + const user_id_to_repo_id_set = try Repo.DB.HashMap(.read_only).init(user_id_to_repo_id_set_cursor); │ │1238 + const user_repos_cursor = try user_id_to_repo_id_set.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; │ │1239 + const user_repos = try Repo.DB.SortedSet(.read_only).init(user_repos_cursor); │ │1240 + try std.testing.expectEqual(1, try user_repos.count()); │ │1241 + } │ │1242 + │ │1243 // │ │1244 // remove the user │ │1245 // │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1262 const event_id_to_user_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->user")) orelse return error.NotFound;│ │1263 const event_id_to_user = try Repo.DB.HashMap(.read_only).init(event_id_to_user_cursor); │ │1264 │ │1237 - try std.testing.expect(null == try event_id_to_user.getCursor(hash.hashInt(repo_opts.hash, &user_event_id))); │ │1265 + const user_cursor = try event_id_to_user.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; │ │1266 + const user_map = try Repo.DB.HashMap(.read_only).init(user_cursor); │ │1267 + const user_event = try evt.read(evt.User.Record, Repo.DB, repo_opts.hash, &arena, user_map); │ │1268 + try std.testing.expect(user_event.deleted); │ │1269 + │ │1270 + // deleting the user tombstones their active repos too │ │1271 + const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound;│ │1272 + const event_id_to_repo = try Repo.DB.HashMap(.read_only).init(event_id_to_repo_cursor); │ │1273 + const repo_cursor = try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id)) orelse return error.NotFound; │ │1274 + const repo_map = try Repo.DB.HashMap(.read_only).init(repo_cursor); │ │1275 + const repo_event = try evt.read(evt.Repo.Record, Repo.DB, repo_opts.hash, &arena, repo_map); │ │1276 + try std.testing.expect(repo_event.deleted); │ │1277 } │ │1278 } │ │1279 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯