╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ────╮ │src/event.zig│ ╰─────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1662 @field(event, field.name) = null; │ │1663 } │ │1664 }, │ │1664 - // a struct field's own fields are read from the same map │ │1665 - .@"struct" => @field(event, field.name) = try read(field.type, DB, hash_kind, arena, map), │ │1665 + .@"struct" => { │ │1666 + const cursor = try map.getCursor(hash.hashInt(hash_kind, field.name)) orelse return error.NotFound;│ │1667 + @field(event, field.name) = try read( │ │1668 + field.type, │ │1669 + DB, │ │1670 + hash_kind, │ │1671 + arena, │ │1672 + try DB.HashMap(.read_only).init(cursor), │ │1673 + ); │ │1674 + }, │ │1675 else => @compileError("unsupported read field type: " ++ @typeName(field.type)), │ │1676 } │ │1677 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────╮ │1713 } │ │1714 }, │ │1715 .bool, .int, .@"enum" => return a == b, │ │1716 + .@"struct" => return fieldsEqual(Field, a, b), │ │1717 .optional => |optional_info| { │ │1718 if (a) |a_value| { │ │1719 if (b) |b_value| return fieldEqual(optional_info.child, a_value, b_value);│ ╰────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1785 ) !void { │ │1786 switch (@typeInfo(T)) { │ │1787 .@"struct" => |struct_info| { │ │1778 - comptime validateFlattenedFields(T); │ │1788 inline for (struct_info.fields) |field| { │ │1789 try upsertField(DB, hash_kind, map, field.name, field.type, @field(event, field.name));│ │1790 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1863 if (array_info.child != u8) @compileError("unsupported upsert field type: " ++ @typeName(Field));│ │1864 try upsertBytes(DB, hash_kind, map, key, &child); │ │1865 }, │ │1866 + .@"struct" => try upsert( │ │1867 + optional_info.child, │ │1868 + DB, │ │1869 + hash_kind, │ │1870 + try DB.HashMap(.read_write).init(try map.putCursor(key)), │ │1871 + child, │ │1872 + ), │ │1873 else => @compileError("unsupported upsert field type: " ++ @typeName(Field)), │ │1874 } │ │1875 } else { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────╮ │1876 _ = try map.remove(key); │ │1877 } │ │1878 }, │ │1863 - // a struct field's own fields are stored in the same map │ │1864 - .@"struct" => try upsert(Field, DB, hash_kind, map, value), │ │1879 + .@"struct" => try upsert( │ │1880 + Field, │ │1881 + DB, │ │1882 + hash_kind, │ │1883 + try DB.HashMap(.read_write).init(try map.putCursor(key)), │ │1884 + value, │ │1885 + ), │ │1886 else => @compileError("unsupported upsert field type: " ++ @typeName(Field)),│ │1887 } │ │1888 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────────╮ │src/event/Patch.zig│ ╰───────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────╮ │ 7 description: []const u8, │ │ 8 tags: []const u8, // space-separated │ │ 9 target_patch_id: ?[evt.event_id_size * 2]u8 = null, │ │10 -patchrev_id: ?[evt.event_id_size * 2]u8 = null, │ │10 +revision: ?Revision = null, │ │11 status: Status = .open, │ │12 + │ │13 +pub const Revision = struct { │ │14 + id: [evt.event_id_size * 2]u8, │ │15 + squash_oid: []const u8, │ │16 + source_oid: []const u8, │ │17 + target_ref: []const u8, │ │18 + │ │19 + pub fn fromRecord(id: [evt.event_id_size]u8, record: evt.PatchRev.Record) Revision {│ │20 + return .{ │ │21 + .id = std.fmt.bytesToHex(id, .lower), │ │22 + .squash_oid = record.patch_oid, │ │23 + .source_oid = record.event.source_oid, │ │24 + .target_ref = record.event.target_ref, │ │25 + }; │ │26 + } │ │27 + │ │28 + pub fn matches(self: Revision, record: evt.PatchRev.Record) bool { │ │29 + return !record.removed and │ │30 + std.mem.eql(u8, record.patch_oid, self.squash_oid) and │ │31 + std.mem.eql(u8, record.event.source_oid, self.source_oid) and │ │32 + std.mem.eql(u8, record.event.target_ref, self.target_ref); │ │33 + } │ │34 +}; │ │35 │ │36 pub const Record = struct { │ │37 event: Self, │ ╰────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │63 pub const target_patch_id_to_patch_id_set_key = "target-patch-id->patch-id-set";│ │64 pub const status_to_id_set_key = "status->patch-id-set"; │ │65 pub const tag_status_to_id_set_key = "tag+status->patch-id-set"; │ │66 +pub const revision_to_id_set_key = "target-ref+oid->patch-id-set"; │ │67 │ │68 pub const TagStatusKey = [tag_max_len + 1 + Status.longest_len]u8; │ │69 │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │115 }; │ │116 │ │117 if (!fieldsValid(record.event.title, record.event.tags)) return error.InvalidPatch; │ │ 94 - const patchrev_id = if (record.event.patchrev_id) |*id| try evt.parseEventId(id) else null; │ │118 + const revision_id = if (record.event.revision) |*revision| blk: { │ │119 + try evt.PatchRev.validateOid(hash_kind, revision.squash_oid); │ │120 + try evt.PatchRev.validateOid(hash_kind, revision.source_oid); │ │121 + try evt.PatchRev.validateTarget(revision.target_ref); │ │122 + break :blk try evt.parseEventId(&revision.id); │ │123 + } else null; │ │124 const target_patch_id = if (record.event.target_patch_id) |*id| try evt.parseEventId(id) else null;│ │125 if (target_patch_id) |*id| { │ │126 if (std.mem.eql(u8, id, event_id)) return error.InvalidPatch; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │138 } │ │139 │ │140 if (record.event.status == .merged) { │ │112 - const revision_id = patchrev_id orelse return error.InvalidPatch; │ │113 - const revision = (try evt.PatchRev.readById(DB, hash_kind, haxy_moment.readOnly(), arena, &revision_id)) orelse return error.InvalidPatch;│ │114 - if (revision.removed) return error.InvalidPatch; │ │141 + const id = revision_id orelse return error.InvalidPatch; │ │142 + const selected = record.event.revision orelse return error.InvalidPatch; │ │143 + const revision = (try evt.PatchRev.readById(DB, hash_kind, haxy_moment.readOnly(), arena, &id)) orelse return error.InvalidPatch; │ │144 + if (!selected.matches(revision)) return error.InvalidPatch; │ │145 } │ │146 │ │147 const order_key = evt.orderKeyDesc(record.created_order, event_id); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │151 const old_status = try statusSet(DB, statuses, existing.event.status); │ │152 _ = try old_status.remove(&order_key); │ │153 try removeFromTagSets(DB, tag_statuses, existing.event.tags, existing.event.status, &order_key); │ │154 + if (existing.event.status != .merged) { │ │155 + if (existing.event.revision) |revision| { │ │156 + const revisions = try DB.HashMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, revision_to_id_set_key)));│ │157 + for ([_][]const u8{ revision.squash_oid, revision.source_oid }, 0..) |oid, i| { │ │158 + if (i != 0 and std.mem.eql(u8, revision.squash_oid, revision.source_oid)) continue; │ │159 + const key_hash = hash.hashInt(hash_kind, try revisionKey(arena.allocator(), revision.target_ref, oid)); │ │160 + const ids = try DB.CountedHashSet(.read_write).init(try revisions.putCursor(key_hash)); │ │161 + _ = try ids.remove(record_key); │ │162 + if (try ids.count() == 0) _ = try revisions.remove(key_hash); │ │163 + } │ │164 + } │ │165 + } │ │166 } │ │167 } │ │168 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯