╭ changed ──────────╮ │src/event/Issue.zig│ ╰───────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────╮ │13 // what the db stores: the event's data plus the commit-derived fields │ │14 pub const Record = struct { │ │15 event: Self, │ │16 + deleted: bool = false, │ │17 author_email: ?[]const u8 = null, │ │18 created_ts: u64 = 0, // the commit timestamp of the event that first created this issue│ │19 }; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 92 const issue_id_to_field_to_oid_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, id_to_field_to_oid_key)); │ │ 93 const issue_id_to_field_to_oid = try DB.HashMap(.read_write).init(issue_id_to_field_to_oid_cursor); │ │ 94 │ │ 94 - if (record_maybe) |record| { │ │ 95 - var record_to_write = record; │ │ 95 + var existing_record_maybe: ?Record = null; │ │ 96 + const existing_cursor_maybe = try event_id_to_issue.getCursor(issue_key); │ │ 97 + if (existing_cursor_maybe) |existing_cursor| { │ │ 98 + const existing_issue = try DB.HashMap(.read_only).init(existing_cursor); │ │ 99 + existing_record_maybe = try evt.read(Record, DB, hash_kind, arena, existing_issue); │ │100 + } │ │101 │ │ 97 - var existing_record_maybe: ?Record = null; │ │ 98 - const existing_cursor_maybe = try event_id_to_issue.getCursor(issue_key); │ │ 99 - if (existing_cursor_maybe) |existing_cursor| { │ │100 - // updates preserve the original creation timestamp and author │ │101 - const existing_issue = try DB.HashMap(.read_only).init(existing_cursor); │ │102 - const existing_record = try evt.read(Record, DB, hash_kind, arena, existing_issue); │ │103 - existing_record_maybe = existing_record; │ │104 - record_to_write.created_ts = existing_record.created_ts; │ │105 - record_to_write.author_email = existing_record.author_email; │ │102 + var record_to_write = if (record_maybe) |record| │ │103 + record │ │104 + else blk: { │ │105 + var record = existing_record_maybe orelse return error.EventNotFound; │ │106 + record.deleted = true; │ │107 + break :blk record; │ │108 + }; │ │109 + │ │110 + if (existing_record_maybe) |existing_record| { │ │111 + // updates preserve the original creation timestamp and author │ │112 + record_to_write.created_ts = existing_record.created_ts; │ │113 + record_to_write.author_email = existing_record.author_email; │ │114 │ │107 - // drop the old status's and tags' entries; the current ones are re-added below │ │108 - const order_key = evt.orderKeyDesc(existing_record.created_ts, event_id); │ │115 + // drop the old status's and tags' entries; active values are re-added below │ │116 + const order_key = evt.orderKeyDesc(existing_record.created_ts, event_id); │ │117 + if (!existing_record.deleted) { │ │118 const status_set = try statusSet(DB, status_to_issues, existing_record.event.status); │ │119 _ = try status_set.remove(&order_key); │ │120 try removeFromTagSets(DB, tag_to_issues, existing_record.event.tags, existing_record.event.status, &order_key);│ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │112 - │ │113 - // any event settles the conflict, since resolving in the ui may │ │114 - // keep our own values and so change nothing to detect │ │115 - if (event_oid != null) _ = try conflicts.remove(&order_key); │ │121 } │ │122 │ │118 - try evt.writeOid(Self, DB, hash_kind, issue_id_to_field_to_oid, issue_key, if (existing_record_maybe) |existing| existing.event else null, record.event, event_oid); │ │123 + // any event settles the conflict, since resolving in the ui may │ │124 + // keep our own values and so change nothing to detect │ │125 + if (event_oid != null or record_to_write.deleted) _ = try conflicts.remove(&order_key); │ │126 + } │ │127 │ │120 - const issue_cursor = try event_id_to_issue.putCursor(issue_key); │ │121 - const issue = try DB.HashMap(.read_write).init(issue_cursor); │ │122 - try evt.upsert(Record, DB, hash_kind, issue, record_to_write); │ │128 + try evt.writeOid(Self, DB, hash_kind, issue_id_to_field_to_oid, issue_key, if (existing_record_maybe) |existing| existing.event else null, record_to_write.event, event_oid);│ │129 │ │124 - const order_key = evt.orderKeyDesc(record_to_write.created_ts, event_id); │ │130 + const issue_cursor = try event_id_to_issue.putCursor(issue_key); │ │131 + const issue = try DB.HashMap(.read_write).init(issue_cursor); │ │132 + try evt.upsert(Record, DB, hash_kind, issue, record_to_write); │ │133 │ │126 - // first time we've seen this issue: add it to the set that enumerates │ │127 - // every issue regardless of status │ │128 - if (existing_cursor_maybe == null) { │ │129 - const issue_id_set_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, id_set_key)); │ │130 - const issue_id_set = try DB.SortedSet(.read_write).init(issue_id_set_cursor); │ │131 - try issue_id_set.put(&order_key); │ │132 - } │ │134 + const order_key = evt.orderKeyDesc(record_to_write.created_ts, event_id); │ │135 │ │134 - const status_set = try statusSet(DB, status_to_issues, record.event.status); │ │136 + // the id set retains tombstones so merges can carry deletions │ │137 + if (existing_cursor_maybe == null) { │ │138 + const issue_id_set_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, id_set_key)); │ │139 + const issue_id_set = try DB.SortedSet(.read_write).init(issue_id_set_cursor); │ │140 + try issue_id_set.put(&order_key); │ │141 + } │ │142 + │ │143 + if (!record_to_write.deleted) { │ │144 + const status_set = try statusSet(DB, status_to_issues, record_to_write.event.status); │ │145 try status_set.put(&order_key); │ │146 │ │137 - var tag_iter = tagIterator(record.event.tags); │ │147 + var tag_iter = tagIterator(record_to_write.event.tags); │ │148 while (tag_iter.next()) |tag| { │ │149 // an over-long tag goes unindexed rather than failing the consume, │ │150 // which would wedge the branch it arrived on │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │151 if (tag.len == 0 or tag.len > tag_max_len) continue; │ │152 var key_buffer: TagStatusKey = undefined; │ │143 - const tag_set_cursor = try tag_to_issues.putCursor(try tagStatusKey(&key_buffer, tag, record.event.status)); │ │153 + const tag_set_cursor = try tag_to_issues.putCursor(try tagStatusKey(&key_buffer, tag, record_to_write.event.status));│ │154 const tag_set = try DB.SortedSet(.read_write).init(tag_set_cursor); │ │155 try tag_set.put(&order_key); │ │156 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │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 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯