╭ changed ─────────╮ │src/event/User.zig│ ╰──────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │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")); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯