╭ changed ────╮ │src/event.zig│ ╰─────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭─────────────────────────────────────────────────────────╮ │12 pub const Comment = @import("event/Comment.zig"); │ │13 │ │14 pub const event_id_size: usize = 16; │ │15 + │ │16 +pub const MergePolicy = enum { │ │17 + field_conflicts, │ │18 + target_wins, │ │19 +}; │ │20 │ │21 // the most bytes an event's serialized form may hold│ │22 pub const max_event_size: usize = 100 * 1024; │ ╰─────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │758 return merged; │ │759 } │ │760 │ │756 -// three-way merge of one kind's records from a merge parent, field by field, so│ │757 -// sides editing different fields combine cleanly. the result goes through the │ │758 -// kind's `consume`, which re-derives the indexes from it. a field both sides │ │759 -// changed differently keeps the target's value and records a conflict entry. a │ │761 +// three-way merge of one kind's records from a merge parent. kinds that expose │ │762 +// conflicts merge field by field; the others take one whole record. the result │ │763 +// goes through the kind's `consume`, which re-derives the indexes from it. a │ │764 // deletion carries over unless the other side changed the record, so a merge │ │765 // never fails. │ │766 pub fn merge( │ ╰─────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │784 } │ │785 │ │786 var parent_conflicts: ?DB.SortedMap(.read_only) = null; │ │783 - if (try parent_moment.getCursor(hash.hashInt(hash_kind, T.conflicts_key))) |cursor| { │ │784 - parent_conflicts = try DB.SortedMap(.read_only).init(cursor); │ │785 - } │ │786 - │ │787 var baseline_conflicts: ?DB.SortedMap(.read_only) = null; │ │788 - if (try baseline_moment.getCursor(hash.hashInt(hash_kind, T.conflicts_key))) |cursor| { │ │789 - baseline_conflicts = try DB.SortedMap(.read_only).init(cursor); │ │790 - } │ │791 - │ │788 var parent_id_to_field_to_oid: ?DB.HashMap(.read_only) = null; │ │793 - if (try parent_moment.getCursor(hash.hashInt(hash_kind, T.id_to_field_to_oid_key))) |cursor| { │ │794 - parent_id_to_field_to_oid = try DB.HashMap(.read_only).init(cursor); │ │789 + switch (comptime T.merge_policy) { │ │790 + .field_conflicts => { │ │791 + if (try parent_moment.getCursor(hash.hashInt(hash_kind, T.conflicts_key))) |cursor| { │ │792 + parent_conflicts = try DB.SortedMap(.read_only).init(cursor); │ │793 + } │ │794 + if (try baseline_moment.getCursor(hash.hashInt(hash_kind, T.conflicts_key))) |cursor| { │ │795 + baseline_conflicts = try DB.SortedMap(.read_only).init(cursor); │ │796 + } │ │797 + if (try parent_moment.getCursor(hash.hashInt(hash_kind, T.id_to_field_to_oid_key))) |cursor| {│ │798 + parent_id_to_field_to_oid = try DB.HashMap(.read_only).init(cursor); │ │799 + } │ │800 + }, │ │801 + .target_wins => {}, │ │802 } │ │803 │ │804 var arena = std.heap.ArenaAllocator.init(allocator); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │844 } │ │845 } │ │846 │ │840 - outcome = @splat(.kept); │ │841 - merged = target_record; │ │842 - merged.event = mergeFields(T, if (baseline_record) |baseline| baseline.event else null, target_record.event, parent_record.event, &outcome); │ │847 + switch (comptime T.merge_policy) { │ │848 + .field_conflicts => { │ │849 + outcome = @splat(.kept); │ │850 + merged = target_record; │ │851 + merged.event = mergeFields(T, if (baseline_record) |baseline| baseline.event else null, target_record.event, parent_record.event, &outcome);│ │852 │ │844 - // merge the tombstone separately from serialized fields. a deletion │ │845 - // carries over an unchanged record, while an edit or restoration │ │846 - // keeps it active. │ │847 - merged.deleted = if (target_record.deleted == parent_record.deleted) │ │848 - target_record.deleted │ │849 - else if (baseline_record) |baseline| blk: { │ │850 - const active = if (!target_record.deleted) target_record else parent_record; │ │851 - if (active.deleted != baseline.deleted) break :blk false; │ │852 - if (!fieldsEqual(T, active.event, baseline.event)) break :blk false; │ │853 - break :blk true; │ │854 - } else false; │ │853 + // merge the tombstone separately from serialized fields. a deletion │ │854 + // carries over an unchanged record, while an edit or restoration │ │855 + // keeps it active. │ │856 + merged.deleted = if (target_record.deleted == parent_record.deleted) │ │857 + target_record.deleted │ │858 + else if (baseline_record) |baseline| blk: { │ │859 + const active = if (!target_record.deleted) target_record else parent_record; │ │860 + if (active.deleted != baseline.deleted) break :blk false; │ │861 + if (!fieldsEqual(T, active.event, baseline.event)) break :blk false; │ │862 + break :blk true; │ │863 + } else false; │ │864 + }, │ │865 + .target_wins => { │ │866 + // an uncontested parent change carries over. if both sides │ │867 + // changed the record, the target wins as a whole, except that │ │868 + // an edit or restoration wins over a deletion. │ │869 + const target_changed = if (baseline_slot) |slot| │ │870 + !target_record_cursor.slot().eql(slot) │ │871 + else │ │872 + true; │ │873 + merged = if (!target_changed) │ │874 + parent_record │ │875 + else if (target_record.deleted != parent_record.deleted) │ │876 + if (target_record.deleted) parent_record else target_record │ │877 + else │ │878 + target_record; │ │879 + }, │ │880 + } │ │881 } │ │882 │ │883 // the index is unique, so a key the target gave to a different record │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────╮ │898 // a different side │ │899 try T.consume(DB, hash_kind, haxy_moment, &event_id, merged, &arena, null);│ │900 │ │901 + switch (comptime T.merge_policy) { │ │902 + .field_conflicts => {}, │ │903 + .target_wins => continue, │ │904 + } │ │905 + │ │906 // tombstones do not carry field conflicts │ │907 if (merged.deleted) continue; │ │908 │ ╰────────────────────────────────────────────────────────────────────────────────────────╯