╭ changed ────╮ │src/event.zig│ ╰─────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │654 .unsigned => @field(event, field.name) = @intCast(try readUint(DB, hash_kind, map, field.name)), │ │655 .signed => @field(event, field.name) = @intCast(try readInt(DB, hash_kind, map, field.name)), │ │656 }, │ │657 + .@"enum" => { │ │658 + const bytes = try readBytes(DB, hash_kind, arena.allocator(), map, field.name); │ │659 + @field(event, field.name) = std.meta.stringToEnum(field.type, bytes) orelse return error.InvalidEnumTag;│ │660 + }, │ │661 else => @compileError("unsupported read field type: " ++ @typeName(field.type)), │ │662 } │ │663 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────╮ │736 } │ │737 } │ │738 │ │739 +// three-way merge of a hash map │ │740 fn mergeChangedMapEntries( │ │741 comptime DB: type, │ │742 allocator: std.mem.Allocator,│ ╰──────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────╮ │819 } │ │820 } │ │821 │ │817 -// three-way merge of a sorted map keyed by byte strings, like │ │818 -// mergeChangedMapEntries but recursing into sorted-set values.│ │822 +// three-way merge of a sorted map │ │823 fn mergeChangedSortedMapEntries( │ │824 comptime DB: type, │ │825 allocator: std.mem.Allocator, │ ╰────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │982 │ │983 try map.put(key, .{ .bytes_object = .{ .value = bytes, .format_tag = "bl".* } });│ │984 }, │ │985 + .@"enum" => try upsertBytes(DB, hash_kind, map, key, @tagName(value)), │ │986 else => @compileError("unsupported upsert field type: " ++ @typeName(Field)), │ │987 } │ │988 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────────╮ │src/event/Issue.zig│ ╰───────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────╮ │ 6 title: []const u8, │ │ 7 description: []const u8, │ │ 8 tags: []const u8, // comma-separated │ │ 9 +status: Status = .open, │ │10 created_ts: u64 = 0, // the commit timestamp of the event that first created this issue │ │11 + │ │12 +pub const Status = enum { │ │13 + open, │ │14 + closed, │ │15 + │ │16 + const longest_len = blk: { │ │17 + var len: usize = 0; │ │18 + for (@typeInfo(Status).@"enum".fields) |field| len = @max(len, field.name.len); │ │19 + break :blk len; │ │20 + }; │ │21 +}; │ │22 │ │23 pub const tag_max_len = 64; │ │24 + │ │25 +// a "tag,status" key names a tag's per-status issue set │ │26 +pub const TagStatusKey = [tag_max_len + 1 + Status.longest_len]u8; │ │27 │ │28 +pub fn tagStatusKey(buffer: *TagStatusKey, tag: []const u8, status: Status) ![]const u8 {│ │29 + return std.fmt.bufPrint(buffer, "{s},{s}", .{ tag, @tagName(status) }); │ │30 +} │ │31 + │ │32 pub fn tagIterator(tags: []const u8) std.mem.SplitIterator(u8, .scalar) { │ │33 return std.mem.splitScalar(u8, tags, ','); │ │34 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │59 const event_id_to_issue_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "event-id->issue")); │ │60 const event_id_to_issue = try DB.HashMap(.read_write).init(event_id_to_issue_cursor); │ │61 │ │43 - // maps each tag to the set of issues carrying it, so a tag page can seek │ │44 - // its window the same way the unfiltered issue list does. │ │45 - const tag_to_issues_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "tag->issue-id-set")); │ │62 + // the per-status sets the issues views list, ordered by creation time, │ │63 + // keyed by status name │ │64 + const status_to_issues_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "status->issue-id-set")); │ │65 + const status_to_issues = try DB.SortedMap(.read_write).init(status_to_issues_cursor); │ │66 + │ │67 + // the per-tag, per-status sets, keyed "tag,status" │ │68 + const tag_to_issues_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "tag+status->issue-id-set"));│ │69 const tag_to_issues = try DB.SortedMap(.read_write).init(tag_to_issues_cursor); │ │70 │ │71 if (event_maybe) |event| { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │78 const existing_event = try evt.read(@This(), DB, hash_kind, arena, existing_issue); │ │79 event_to_write.created_ts = existing_event.created_ts; │ │80 │ │58 - // drop the old tags' entries; the current tags are re-added below │ │81 + // drop the old status's and tags' entries; the current ones are re-added below │ │82 const order_key = evt.orderKey(existing_event.created_ts, event_id); │ │60 - try removeFromTagSets(DB, tag_to_issues, existing_event.tags, &order_key); │ │83 + const status_set = try statusSet(DB, status_to_issues, existing_event.status); │ │84 + _ = try status_set.remove(&order_key); │ │85 + try removeFromTagSets(DB, tag_to_issues, existing_event.tags, existing_event.status, &order_key);│ │86 } else { │ │87 // first time we've seen this issue: stamp it with the commit timestamp │ │88 event_to_write.created_ts = created_ts; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │ 94 │ │ 95 const order_key = evt.orderKey(event_to_write.created_ts, event_id); │ │ 96 │ │ 97 + const status_set = try statusSet(DB, status_to_issues, event.status);│ │ 98 + try status_set.put(&order_key); │ │ 99 + │ │100 var tag_iter = tagIterator(event.tags); │ │101 while (tag_iter.next()) |tag| { │ │102 if (tag.len == 0) continue; │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │103 if (tag.len > tag_max_len) return error.TagTooLong; │ │ 76 - const tag_set_cursor = try tag_to_issues.putCursor(tag); │ │104 + var key_buffer: TagStatusKey = undefined; │ │105 + const tag_set_cursor = try tag_to_issues.putCursor(try tagStatusKey(&key_buffer, tag, event.status));│ │106 const tag_set = try DB.SortedSet(.read_write).init(tag_set_cursor); │ │107 try tag_set.put(&order_key); │ │ 79 - } │ │ 80 - │ │ 81 - // first time we've seen this issue: add it to the set the issues view │ │ 82 - // lists, ordered by creation time. the key (orderKey) embeds the event │ │ 83 - // id, so the set needs no value. │ │ 84 - if (existing_cursor_maybe == null) { │ │ 85 - const issue_id_set_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "issue-id-set")); │ │ 86 - const issue_id_set = try DB.SortedSet(.read_write).init(issue_id_set_cursor); │ │ 87 - try issue_id_set.put(&order_key); │ │108 } │ │109 } else { │ │ 90 - // drop it from the ordered sets using its recorded creation timestamp │ │110 + // drop it from the ordered sets using its recorded creation timestamp and status │ │111 if (try event_id_to_issue.getCursor(issue_key)) |existing_cursor| { │ │112 const existing_issue = try DB.HashMap(.read_only).init(existing_cursor); │ │113 const existing_event = try evt.read(@This(), DB, hash_kind, arena, existing_issue); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │114 const order_key = evt.orderKey(existing_event.created_ts, event_id); │ │115 │ │ 96 - const issue_id_set_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "issue-id-set")); │ │ 97 - const issue_id_set = try DB.SortedSet(.read_write).init(issue_id_set_cursor); │ │ 98 - _ = try issue_id_set.remove(&order_key); │ │116 + const status_set = try statusSet(DB, status_to_issues, existing_event.status); │ │117 + _ = try status_set.remove(&order_key); │ │118 │ │100 - try removeFromTagSets(DB, tag_to_issues, existing_event.tags, &order_key); │ │119 + try removeFromTagSets(DB, tag_to_issues, existing_event.tags, existing_event.status, &order_key);│ │120 } │ │121 if (!try event_id_to_issue.remove(issue_key)) return error.EventNotFound; │ │122 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯