╭ changed ────╮ │src/event.zig│ ╰─────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭────────────────────────────────────────╮ │32 user, │ │33 repo, │ │34 issue, │ │35 +}; │ │36 + │ │37 +// a null payload deletes the record│ │38 +pub const Event = union(EventKind) {│ │39 + user: ?User, │ │40 + repo: ?Repo, │ │41 + issue: ?Issue, │ │42 }; │ │43 │ │44 pub const EventWithId = struct { │ ╰────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────╮ │45 id: [event_id_size * 2]u8, │ │39 - event: union(EventKind) { │ │40 - user: ?User, │ │41 - repo: ?Repo, │ │42 - issue: ?Issue, │ │43 - }, │ │46 + event: Event, │ │47 timestamp: u64 = 0, // not serialized, because it comes from the commit timestamp│ │48 │ │49 pub fn jsonStringify(self: EventWithId, jw: anytype) !void { │ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────╮ │424 const haxy_moment = try DB.HashMap(.read_write).init(haxy_moment_cursor);│ │425 │ │426 // merge changes from every parent after the first parent. the common │ │424 - // ancestor is the baseline, so a later parent only contributes values │ │425 - // it changed relative to the merge base. │ │427 + // ancestor is the baseline, so a later parent only contributes records │ │428 + // it changed relative to the merge base. only the record maps are │ │429 + // merged; every other view is derived from them, and each kind's │ │430 + // `consume` re-derives its own as the winning records are written. │ │431 if (parent_oids.len > 1) { │ │432 for (parent_oids[1..]) |*parent_oid| { │ │433 var oid: [hash.byteLen(repo_opts.hash)]u8 = undefined; │ ╰──────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │442 const baseline_haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &baseline_oid)) orelse return error.CursorNotFound;│ │443 const baseline_haxy_moment = try DB.HashMap(.read_only).init(baseline_haxy_moment_cursor); │ │444 │ │440 - try mergeChangedMapEntries(DB, allocator, haxy_moment, parent_haxy_moment, baseline_haxy_moment, true); │ │445 + inline for (std.meta.fields(Event)) |field| { │ │446 + const T = @typeInfo(field.type).optional.child; │ │447 + try merge(T, DB, repo_opts.hash, allocator, haxy_moment, parent_haxy_moment, baseline_haxy_moment); │ │448 + } │ │449 } │ │450 } │ │451 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │472 const created_ts = commit_object.content.commit.metadata.timestamp; │ │473 │ │474 switch (event_with_id.event) { │ │467 - .user => |event_maybe| try User.consume(DB, repo_opts.hash, haxy_moment, &current_event_id, event_maybe, &arena, created_ts), │ │468 - .repo => |event_maybe| try Repo.consume(DB, repo_opts.hash, haxy_moment, &current_event_id, event_maybe, &arena, created_ts), │ │469 - .issue => |event_maybe| try Issue.consume(DB, repo_opts.hash, haxy_moment, &current_event_id, event_maybe, &arena, created_ts), │ │475 + .user => |event_maybe| try User.consume(DB, repo_opts.hash, haxy_moment, &current_event_id, event_maybe, &arena, created_ts, &repo_event_oid), │ │476 + .repo => |event_maybe| try Repo.consume(DB, repo_opts.hash, haxy_moment, &current_event_id, event_maybe, &arena, created_ts, &repo_event_oid), │ │477 + .issue => |event_maybe| try Issue.consume(DB, repo_opts.hash, haxy_moment, &current_event_id, event_maybe, &arena, created_ts, &repo_event_oid),│ │478 } │ │479 } │ │480 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │487 │ │488 if (last_object_id_maybe) |*last_object_id| { │ │489 try moment.put(hash.hashInt(repo_opts.hash, "haxy-last-object-id"), .{ .bytes = last_object_id }); │ │490 + } │ │491 +} │ │492 + │ │493 +// the conflicted records of one kind, keyed by the same order key its id set │ │494 +// uses, so this doubles as the index the conflicts view lists and counts. every │ │495 +// value under an entry is a slot reference to a structure that already exists: │ │496 +// │ │497 +// <order key> -> conflict entry │ │498 +// conflicted-fields the conflicting field names, space separated │ │499 +// base-record the merge base's whole record │ │500 +// their-record the merged-in side's whole record │ │501 +// their-field->oid that side's field name to commit map │ │502 +// │ │503 +// our own value is the live record and our commit is in the kind's oid map, so │ │504 +// neither is repeated here. │ │505 +pub const conflicted_fields_key = "conflicted-fields"; │ │506 +pub const base_record_key = "base-record"; │ │507 +pub const their_record_key = "their-record"; │ │508 +pub const their_field_to_oid_key = "their-field->oid"; │ │509 + │ │510 +// record the commit against every field this event changes. a merge passes no │ │511 +// oid, since each field's winner may come from a different side. │ │512 +pub fn writeOid( │ │513 + comptime T: type, │ │514 + comptime DB: type, │ │515 + comptime hash_kind: hash.HashKind, │ │516 + id_to_field_to_oid: DB.HashMap(.read_write), │ │517 + record_key: hash.HashInt(hash_kind), │ │518 + existing_maybe: ?T, │ │519 + event: T, │ │520 + event_oid: ?[]const u8, │ │521 +) !void { │ │522 + const oid = event_oid orelse return; │ │523 + │ │524 + // created only once a field actually changes │ │525 + var field_oids: ?DB.SortedMap(.read_write) = null; │ │526 + │ │527 + inline for (std.meta.fields(T)) |field| { │ │528 + if (comptime !std.mem.eql(u8, field.name, "created_ts")) { │ │529 + const changed = if (existing_maybe) |existing| │ │530 + !fieldEqual(field.type, @field(existing, field.name), @field(event, field.name)) │ │531 + else │ │532 + true; │ │533 + if (changed) { │ │534 + if (field_oids == null) { │ │535 + field_oids = try DB.SortedMap(.read_write).init(try id_to_field_to_oid.putCursor(record_key)); │ │536 + } │ │537 + if (field_oids) |map| try map.put(field.name, .{ .bytes = oid }); │ │538 + } │ │539 + } │ │540 + } │ │541 +} │ │542 + │ │543 +// the whole field list, space separated, is the longest it can get │ │544 +fn conflictedFieldsMaxLen(comptime T: type) usize { │ │545 + var len: usize = 0; │ │546 + for (std.meta.fields(T)) |field| len += field.name.len + 1; │ │547 + return len; │ │548 +} │ │549 + │ │550 +// what a three-way merge did with a field │ │551 +const FieldMerge = enum { │ │552 + // the target's value stands, because neither side changed it, both made the │ │553 + // same change, or only the target changed it │ │554 + kept, │ │555 + parent, │ │556 + conflicted, │ │557 +}; │ │558 + │ │559 +// three-way merge each field. one only the parent changed takes the parent's │ │560 +// value; one both sides changed differently keeps the target's. a missing │ │561 +// baseline makes every differing field a conflict. │ │562 +fn mergeFields( │ │563 + comptime T: type, │ │564 + baseline_maybe: ?T, │ │565 + target: T, │ │566 + parent: T, │ │567 + outcome: *[std.meta.fields(T).len]FieldMerge, │ │568 +) T { │ │569 + var merged = target; │ │570 + │ │571 + inline for (std.meta.fields(T), 0..) |field, i| { │ │572 + if (comptime !std.mem.eql(u8, field.name, "created_ts")) { │ │573 + const target_value = @field(target, field.name); │ │574 + const parent_value = @field(parent, field.name); │ │575 + │ │576 + if (!fieldEqual(field.type, target_value, parent_value)) { │ │577 + outcome[i] = if (baseline_maybe) |baseline| blk: { │ │578 + const baseline_value = @field(baseline, field.name); │ │579 + if (fieldEqual(field.type, target_value, baseline_value)) { │ │580 + @field(merged, field.name) = parent_value; │ │581 + break :blk .parent; │ │582 + } │ │583 + // only the target changed it │ │584 + if (fieldEqual(field.type, parent_value, baseline_value)) break :blk .kept; │ │585 + break :blk .conflicted; │ │586 + } else .conflicted; │ │587 + } │ │588 + } │ │589 + } │ │590 + │ │591 + return merged; │ │592 +} │ │593 + │ │594 +// three-way merge of one kind's records from a merge parent, field by field, so │ │595 +// sides editing different fields combine cleanly. the result goes through the │ │596 +// kind's `consume`, which re-derives the indexes from it. a field both sides │ │597 +// changed differently keeps the target's value and records a conflict entry. a │ │598 +// deletion carries over unless the other side changed the record, so a merge │ │599 +// never fails. │ │600 +pub fn merge( │ │601 + comptime T: type, │ │602 + comptime DB: type, │ │603 + comptime hash_kind: hash.HashKind, │ │604 + allocator: std.mem.Allocator, │ │605 + haxy_moment: DB.HashMap(.read_write), │ │606 + parent_moment: DB.HashMap(.read_only), │ │607 + baseline_moment: DB.HashMap(.read_only), │ │608 +) !void { │ │609 + const parent_set_cursor = try parent_moment.getCursor(hash.hashInt(hash_kind, T.id_set_key)) orelse return; │ │610 + const parent_set = try DB.SortedSet(.read_only).init(parent_set_cursor); │ │611 + │ │612 + const parent_records_cursor = try parent_moment.getCursor(hash.hashInt(hash_kind, T.record_map_key)) orelse return; │ │613 + const parent_records = try DB.HashMap(.read_only).init(parent_records_cursor); │ │614 + │ │615 + var baseline_records: ?DB.HashMap(.read_only) = null; │ │616 + if (try baseline_moment.getCursor(hash.hashInt(hash_kind, T.record_map_key))) |cursor| { │ │617 + baseline_records = try DB.HashMap(.read_only).init(cursor); │ │618 + } │ │619 + │ │620 + var parent_conflicts: ?DB.SortedMap(.read_only) = null; │ │621 + if (try parent_moment.getCursor(hash.hashInt(hash_kind, T.conflicts_key))) |cursor| { │ │622 + parent_conflicts = try DB.SortedMap(.read_only).init(cursor); │ │623 + } │ │624 + │ │625 + var baseline_conflicts: ?DB.SortedMap(.read_only) = null; │ │626 + if (try baseline_moment.getCursor(hash.hashInt(hash_kind, T.conflicts_key))) |cursor| { │ │627 + baseline_conflicts = try DB.SortedMap(.read_only).init(cursor); │ │628 + } │ │629 + │ │630 + var parent_id_to_field_to_oid: ?DB.HashMap(.read_only) = null; │ │631 + if (try parent_moment.getCursor(hash.hashInt(hash_kind, T.id_to_field_to_oid_key))) |cursor| { │ │632 + parent_id_to_field_to_oid = try DB.HashMap(.read_only).init(cursor); │ │633 + } │ │634 + │ │635 + var arena = std.heap.ArenaAllocator.init(allocator); │ │636 + defer arena.deinit(); │ │637 + │ │638 + // a record the parent deleted is deleted here too, unless the target │ │639 + // changed it. runs first so a delete and recreate is applied in that order. │ │640 + deletions: { │ │641 + const baseline_map = baseline_records orelse break :deletions; │ │642 + const baseline_set_cursor = try baseline_moment.getCursor(hash.hashInt(hash_kind, T.id_set_key)) orelse break :deletions; │ │643 + const baseline_set = try DB.SortedSet(.read_only).init(baseline_set_cursor); │ │644 + │ │645 + var baseline_iter = try baseline_set.iteratorFromIndex(0); │ │646 + while (try baseline_iter.next()) |kv_pair_cursor| { │ │647 + const event_id = try readOrderKeyId(DB, kv_pair_cursor); │ │648 + const record_key = hash.hashInt(hash_kind, &event_id); │ │649 + │ │650 + if (null != try parent_records.getSlot(record_key)) continue; │ │651 + const baseline_record_cursor = try baseline_map.getCursor(record_key) orelse continue; │ │652 + │ │653 + _ = arena.reset(.retain_capacity); │ │654 + │ │655 + // re-derived every iteration, since `consume` writes through its │ │656 + // own handles │ │657 + const target_records_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, T.record_map_key)); │ │658 + const target_records = try DB.HashMap(.read_write).init(target_records_cursor); │ │659 + const target_record_cursor = try target_records.getCursor(record_key) orelse continue; │ │660 + │ │661 + if (!target_record_cursor.slot().eql(baseline_record_cursor.slot())) { │ │662 + // every write copies the record's block, so only a content │ │663 + // change means the target kept it │ │664 + const target_record = try read(T, DB, hash_kind, &arena, try DB.HashMap(.read_only).init(target_record_cursor)); │ │665 + const baseline_record = try read(T, DB, hash_kind, &arena, try DB.HashMap(.read_only).init(baseline_record_cursor)); │ │666 + if (!fieldsEqual(T, target_record, baseline_record)) continue; │ │667 + } │ │668 + │ │669 + try T.consume(DB, hash_kind, haxy_moment, &event_id, null, &arena, 0, null); │ │670 + } │ │671 + } │ │672 + │ │673 + var iter = try parent_set.iteratorFromIndex(0); │ │674 + while (try iter.next()) |kv_pair_cursor| { │ │675 + const event_id = try readOrderKeyId(DB, kv_pair_cursor); │ │676 + const record_key = hash.hashInt(hash_kind, &event_id); │ │677 + │ │678 + _ = arena.reset(.retain_capacity); │ │679 + │ │680 + const parent_record_cursor = try parent_records.getCursor(record_key) orelse continue; │ │681 + const parent_slot = parent_record_cursor.slot(); │ │682 + const baseline_slot = if (baseline_records) |map| try map.getSlot(record_key) else null; │ │683 + │ │684 + // the parent left this record alone │ │685 + if (baseline_slot) |slot| { │ │686 + if (slot.eql(parent_slot)) continue; │ │687 + } │ │688 + │ │689 + const parent_record = try read(T, DB, hash_kind, &arena, try DB.HashMap(.read_only).init(parent_record_cursor)); │ │690 + │ │691 + // re-derived every iteration, since `consume` writes through its own │ │692 + // handles │ │693 + const target_records_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, T.record_map_key)); │ │694 + const target_records = try DB.HashMap(.read_write).init(target_records_cursor); │ │695 + │ │696 + // a record the target doesn't have, or deleted, arrives wholesale from │ │697 + // the parent │ │698 + var outcome: [std.meta.fields(T).len]FieldMerge = @splat(.parent); │ │699 + var merged = parent_record; │ │700 + │ │701 + if (try target_records.getCursor(record_key)) |target_record_cursor| { │ │702 + // both sides share the change │ │703 + if (target_record_cursor.slot().eql(parent_slot)) continue; │ │704 + │ │705 + const target_record = try read(T, DB, hash_kind, &arena, try DB.HashMap(.read_only).init(target_record_cursor)); │ │706 + │ │707 + var baseline_record: ?T = null; │ │708 + if (baseline_records) |map| { │ │709 + if (try map.getCursor(record_key)) |baseline_record_cursor| { │ │710 + baseline_record = try read(T, DB, hash_kind, &arena, try DB.HashMap(.read_only).init(baseline_record_cursor)); │ │711 + } │ │712 + } │ │713 + │ │714 + outcome = @splat(.kept); │ │715 + merged = mergeFields(T, baseline_record, target_record, parent_record, &outcome); │ │716 + } │ │717 + │ │718 + // the index is unique, so a key the target gave to a different record │ │719 + // leaves this one out rather than stranding that one behind a name it │ │720 + // no longer owns │ │721 + if (@hasDecl(T, "name_index_key")) { │ │722 + const index_key = try merged.indexKey(arena.allocator()); │ │723 + const name_index_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, T.name_index_key)); │ │724 + const name_index = try DB.HashMap(.read_write).init(name_index_cursor); │ │725 + if (try name_index.getCursor(hash.hashInt(hash_kind, index_key))) |owner_cursor| { │ │726 + var owner_id: [event_id_size]u8 = undefined; │ │727 + _ = try owner_cursor.readBytes(&owner_id); │ │728 + if (!std.mem.eql(u8, &owner_id, &event_id)) continue; │ │729 + } │ │730 + } │ │731 + │ │732 + // `consume` sets no oids, since each field's winner may come from │ │733 + // a different side │ │734 + try T.consume(DB, hash_kind, haxy_moment, &event_id, merged, &arena, merged.created_ts, null); │ │735 + │ │736 + const conflicts_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, T.conflicts_key)); │ │737 + const conflicts = try DB.SortedMap(.read_write).init(conflicts_cursor); │ │738 + const id_to_field_to_oid_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, T.id_to_field_to_oid_key)); │ │739 + const id_to_field_to_oid = try DB.HashMap(.read_write).init(id_to_field_to_oid_cursor); │ │740 + const merged_key = orderKeyDesc(merged.created_ts, &event_id); │ │741 + │ │742 + // both created only once a field actually comes from the parent │ │743 + var parent_field_oids: ?DB.SortedMap(.read_only) = null; │ │744 + if (parent_id_to_field_to_oid) |map| { │ │745 + if (try map.getCursor(record_key)) |field_oids_cursor| { │ │746 + parent_field_oids = try DB.SortedMap(.read_only).init(field_oids_cursor); │ │747 + } │ │748 + } │ │749 + var field_oids: ?DB.SortedMap(.read_write) = null; │ │750 + │ │751 + // the fields both sides changed differently, space separated │ │752 + var conflicted_fields: [conflictedFieldsMaxLen(T)]u8 = undefined; │ │753 + var conflicted_len: usize = 0; │ │754 + │ │755 + inline for (std.meta.fields(T), 0..) |field, i| { │ │756 + if (comptime !std.mem.eql(u8, field.name, "created_ts")) { │ │757 + switch (outcome[i]) { │ │758 + // the target's value stands, so its oid does too │ │759 + .kept => {}, │ │760 + .parent => if (parent_field_oids) |parent_map| { │ │761 + if (try parent_map.getCursor(field.name)) |oid_cursor| { │ │762 + if (field_oids == null) { │ │763 + field_oids = try DB.SortedMap(.read_write).init(try id_to_field_to_oid.putCursor(record_key)); │ │764 + } │ │765 + if (field_oids) |map| try map.put(field.name, .{ .slot = oid_cursor.slot() }); │ │766 + } │ │767 + }, │ │768 + .conflicted => { │ │769 + if (conflicted_len > 0) { │ │770 + conflicted_fields[conflicted_len] = ' '; │ │771 + conflicted_len += 1; │ │772 + } │ │773 + @memcpy(conflicted_fields[conflicted_len..][0..field.name.len], field.name); │ │774 + conflicted_len += field.name.len; │ │775 + }, │ │776 + } │ │777 + } │ │778 + } │ │779 + │ │780 + if (conflicted_len > 0) { │ │781 + // each side's whole record is stored by reference, so the view can │ │782 + // show any of them or take theirs wholesale │ │783 + _ = try conflicts.remove(&merged_key); │ │784 + const conflict = try DB.HashMap(.read_write).init(try conflicts.putCursor(&merged_key)); │ │785 + try conflict.put(hash.hashInt(hash_kind, conflicted_fields_key), .{ .bytes = conflicted_fields[0..conflicted_len] }); │ │786 + try conflict.put(hash.hashInt(hash_kind, base_record_key), .{ .slot = baseline_slot }); │ │787 + try conflict.put(hash.hashInt(hash_kind, their_record_key), .{ .slot = parent_slot }); │ │788 + if (parent_field_oids) |map| { │ │789 + try conflict.put(hash.hashInt(hash_kind, their_field_to_oid_key), .{ .slot = map.slot() }); │ │790 + } │ │791 + continue; │ │792 + } │ │793 + │ │794 + // created_ts never changes, so the parent keys this record's conflict │ │795 + // identically, and it carries over when the target has none, unless the │ │796 + // parent's entry is unchanged from the baseline, meaning the target │ │797 + // inherited it too and resolving it removed it here. │ │798 + if (parent_conflicts) |map| { │ │799 + if (null == try conflicts.getCursor(&merged_key)) { │ │800 + if (try map.getSlot(&merged_key)) |conflict_slot| { │ │801 + const baseline_conflict_slot = if (baseline_conflicts) |baseline_map| try baseline_map.getSlot(&merged_key) else null;│ │802 + const resolved = if (baseline_conflict_slot) |slot| slot.eql(conflict_slot) else false; │ │803 + if (!resolved) try conflicts.put(&merged_key, .{ .slot = conflict_slot }); │ │804 + } │ │805 + } │ │806 + } │ │807 } │ │808 } │ │809 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │897 │ │898 pub fn orderKeyDesc(timestamp: u64, event_id: *const [event_id_size]u8) [@sizeOf(u64) + event_id_size]u8 {│ │899 return orderKey(std.math.maxInt(u64) - timestamp, event_id); │ │900 +} │ │901 + │ │902 +// the event id embedded in a set entry's order key │ │903 +fn readOrderKeyId(comptime DB: type, kv_pair_cursor: DB.Cursor(.read_only)) ![event_id_size]u8 { │ │904 + var cursor = kv_pair_cursor; │ │905 + const kv_pair = try cursor.readKeyValuePair(); │ │906 + var order_key: [@sizeOf(u64) + event_id_size]u8 = undefined; │ │907 + _ = try kv_pair.key_cursor.readBytes(&order_key); │ │908 + return order_key[@sizeOf(u64)..][0..event_id_size].*; │ │909 } │ │910 │ │911 // split a pushed "<owner>/<repo>" path into its two components, or null if it │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1026 return event; │ │1027 } │ │1028 │ │1029 +// whether two events carry the same data. `created_ts` comes from the commit │ │1030 +// rather than the payload, so it isn't compared. │ │1031 +fn fieldsEqual(comptime T: type, a: T, b: T) bool { │ │1032 + switch (@typeInfo(T)) { │ │1033 + .@"struct" => |struct_info| { │ │1034 + inline for (struct_info.fields) |field| { │ │1035 + if (comptime std.mem.eql(u8, field.name, "created_ts")) continue; │ │1036 + if (!fieldEqual(field.type, @field(a, field.name), @field(b, field.name))) return false;│ │1037 + } │ │1038 + }, │ │1039 + else => @compileError("fieldsEqual expects a struct"), │ │1040 + } │ │1041 + │ │1042 + return true; │ │1043 +} │ │1044 + │ │1045 +pub fn fieldEqual(comptime Field: type, a: Field, b: Field) bool { │ │1046 + switch (@typeInfo(Field)) { │ │1047 + .pointer => |pointer_info| { │ │1048 + if (pointer_info.size == .slice and pointer_info.child == u8) { │ │1049 + return std.mem.eql(u8, a, b); │ │1050 + } else { │ │1051 + @compileError("unsupported field type: " ++ @typeName(Field)); │ │1052 + } │ │1053 + }, │ │1054 + .array => |array_info| { │ │1055 + if (array_info.child == u8) { │ │1056 + return std.mem.eql(u8, &a, &b); │ │1057 + } else { │ │1058 + @compileError("unsupported field type: " ++ @typeName(Field)); │ │1059 + } │ │1060 + }, │ │1061 + .bool, .int, .@"enum" => return a == b, │ │1062 + else => @compileError("unsupported field type: " ++ @typeName(Field)), │ │1063 + } │ │1064 +} │ │1065 + │ │1066 fn readBytes( │ │1067 comptime DB: type, │ │1068 comptime hash_kind: hash.HashKind, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1128 } │ │1129 }, │ │1130 else => @compileError("upsert expects a struct"), │ │ 760 - } │ │ 761 -} │ │ 762 - │ │ 763 -// three-way merge of a hash map │ │ 764 -fn mergeChangedMapEntries( │ │ 765 - comptime DB: type, │ │ 766 - allocator: std.mem.Allocator, │ │ 767 - target: DB.HashMap(.read_write), │ │ 768 - parent: DB.HashMap(.read_only), │ │ 769 - baseline: DB.HashMap(.read_only), │ │ 770 - comptime is_top_level: bool, │ │ 771 -) !void { │ │ 772 - var parent_iter = try parent.iterator(); │ │ 773 - while (try parent_iter.next()) |kv_pair_cursor| { │ │ 774 - const kv_pair = try kv_pair_cursor.readKeyValuePair(); │ │ 775 - │ │ 776 - const baseline_value_cursor = try baseline.getCursor(kv_pair.hash) orelse { │ │ 777 - if ((try target.getCursor(kv_pair.hash)) != null) { │ │ 778 - return error.MergeConflict; │ │ 779 - } │ │ 780 - │ │ 781 - try target.put(kv_pair.hash, .{ .slot = kv_pair.value_cursor.slot() }); │ │ 782 - continue; │ │ 783 - }; │ │ 784 - │ │ 785 - const tag = kv_pair.value_cursor.slot().tag; │ │ 786 - │ │ 787 - if (tag != baseline_value_cursor.slot().tag) { │ │ 788 - return error.UnexpectedTag; │ │ 789 - } │ │ 790 - │ │ 791 - if (kv_pair.value_cursor.slot().value == baseline_value_cursor.slot().value) { │ │ 792 - continue; │ │ 793 - } │ │ 794 - │ │ 795 - if (tag == .hash_map) { │ │ 796 - const target_existing_cursor = try target.getCursor(kv_pair.hash) orelse return error.MergeConflict;│ │ 797 - if (target_existing_cursor.slot().tag != .hash_map) { │ │ 798 - return error.UnexpectedTag; │ │ 799 - } │ │ 800 - │ │ 801 - const target_child_cursor = try target.putCursor(kv_pair.hash); │ │ 802 - const target_child = try DB.HashMap(.read_write).init(target_child_cursor); │ │ 803 - const parent_child = try DB.HashMap(.read_only).init(kv_pair.value_cursor); │ │ 804 - const baseline_child = try DB.HashMap(.read_only).init(baseline_value_cursor); │ │ 805 - try mergeChangedMapEntries(DB, allocator, target_child, parent_child, baseline_child, false); │ │ 806 - } else if (tag == .sorted_set) { │ │ 807 - const target_existing_cursor = try target.getCursor(kv_pair.hash) orelse return error.MergeConflict;│ │ 808 - if (target_existing_cursor.slot().tag != .sorted_set) { │ │ 809 - return error.UnexpectedTag; │ │ 810 - } │ │ 811 - │ │ 812 - const target_child_cursor = try target.putCursor(kv_pair.hash); │ │ 813 - const target_child = try DB.SortedSet(.read_write).init(target_child_cursor); │ │ 814 - const parent_child = try DB.SortedSet(.read_only).init(kv_pair.value_cursor); │ │ 815 - const baseline_child = try DB.SortedSet(.read_only).init(baseline_value_cursor); │ │ 816 - try mergeChangedSetEntries(DB, allocator, target_child, parent_child, baseline_child); │ │ 817 - } else if (tag == .sorted_map) { │ │ 818 - const target_existing_cursor = try target.getCursor(kv_pair.hash) orelse return error.MergeConflict;│ │ 819 - if (target_existing_cursor.slot().tag != .sorted_map) { │ │ 820 - return error.UnexpectedTag; │ │ 821 - } │ │ 822 - │ │ 823 - const target_child_cursor = try target.putCursor(kv_pair.hash); │ │ 824 - const target_child = try DB.SortedMap(.read_write).init(target_child_cursor); │ │ 825 - const parent_child = try DB.SortedMap(.read_only).init(kv_pair.value_cursor); │ │ 826 - const baseline_child = try DB.SortedMap(.read_only).init(baseline_value_cursor); │ │ 827 - try mergeChangedSortedMapEntries(DB, allocator, target_child, parent_child, baseline_child); │ │ 828 - } else if (is_top_level) { │ │ 829 - // the moment-index is a top-level key in the haxy moment │ │ 830 - // and is not a map. we don't want to do any merge of this. │ │ 831 - continue; │ │ 832 - } else { │ │ 833 - const target_value_cursor = try target.getCursor(kv_pair.hash) orelse return error.MergeConflict; │ │ 834 - if (target_value_cursor.slot().tag != baseline_value_cursor.slot().tag) { │ │ 835 - return error.UnexpectedTag; │ │ 836 - } │ │ 837 - if (target_value_cursor.slot().value != baseline_value_cursor.slot().value) { │ │ 838 - return error.MergeConflict; │ │ 839 - } │ │ 840 - │ │ 841 - try target.put(kv_pair.hash, .{ .slot = kv_pair.value_cursor.slot() }); │ │ 842 - } │ │1131 } │ │1132 } │ │ 845 - │ │ 846 -// three-way merge of a sorted map │ │ 847 -fn mergeChangedSortedMapEntries( │ │ 848 - comptime DB: type, │ │ 849 - allocator: std.mem.Allocator, │ │ 850 - target: DB.SortedMap(.read_write), │ │ 851 - parent: DB.SortedMap(.read_only), │ │ 852 - baseline: DB.SortedMap(.read_only), │ │ 853 -) !void { │ │ 854 - var parent_iter = try parent.iterator(); │ │ 855 - while (try parent_iter.next()) |kv_pair_cursor| { │ │ 856 - const kv_pair = try kv_pair_cursor.readKeyValuePair(); │ │ 857 - const key = try kv_pair.key_cursor.readBytesAlloc(allocator, null); │ │ 858 - defer allocator.free(key); │ │ 859 - │ │ 860 - const baseline_value_cursor = try baseline.getCursor(key) orelse { │ │ 861 - if (try target.getCursor(key)) |target_existing_cursor| { │ │ 862 - // both parents created this key. sets union (they hold │ │ 863 - // independently added members); anything else conflicts. │ │ 864 - const parent_tag = kv_pair.value_cursor.slot().tag; │ │ 865 - if (target_existing_cursor.slot().tag != parent_tag) { │ │ 866 - return error.UnexpectedTag; │ │ 867 - } │ │ 868 - if (parent_tag != .sorted_set) { │ │ 869 - return error.MergeConflict; │ │ 870 - } │ │1133 │ │ 872 - const target_child_cursor = try target.putCursor(key); │ │ 873 - const target_child = try DB.SortedSet(.read_write).init(target_child_cursor); │ │ 874 - const parent_child = try DB.SortedSet(.read_only).init(kv_pair.value_cursor); │ │ 875 - var set_iter = try parent_child.iterator(); │ │ 876 - while (try set_iter.next()) |set_kv_cursor| { │ │ 877 - const set_kv = try set_kv_cursor.readKeyValuePair(); │ │ 878 - const set_key = try set_kv.key_cursor.readBytesAlloc(allocator, null); │ │ 879 - defer allocator.free(set_key); │ │ 880 - try target_child.put(set_key); │ │ 881 - } │ │ 882 - continue; │ │ 883 - } │ │ 884 - │ │ 885 - try target.put(key, .{ .slot = kv_pair.value_cursor.slot() }); │ │ 886 - continue; │ │ 887 - }; │ │ 888 - │ │ 889 - const tag = kv_pair.value_cursor.slot().tag; │ │ 890 - │ │ 891 - if (tag != baseline_value_cursor.slot().tag) { │ │ 892 - return error.UnexpectedTag; │ │ 893 - } │ │ 894 - │ │ 895 - if (kv_pair.value_cursor.slot().value == baseline_value_cursor.slot().value) { │ │ 896 - continue; │ │ 897 - } │ │ 898 - │ │ 899 - if (tag == .sorted_set) { │ │ 900 - const target_existing_cursor = try target.getCursor(key) orelse return error.MergeConflict; │ │ 901 - if (target_existing_cursor.slot().tag != .sorted_set) { │ │ 902 - return error.UnexpectedTag; │ │ 903 - } │ │ 904 - │ │ 905 - const target_child_cursor = try target.putCursor(key); │ │ 906 - const target_child = try DB.SortedSet(.read_write).init(target_child_cursor); │ │ 907 - const parent_child = try DB.SortedSet(.read_only).init(kv_pair.value_cursor); │ │ 908 - const baseline_child = try DB.SortedSet(.read_only).init(baseline_value_cursor); │ │ 909 - try mergeChangedSetEntries(DB, allocator, target_child, parent_child, baseline_child); │ │ 910 - } else { │ │ 911 - const target_value_cursor = try target.getCursor(key) orelse return error.MergeConflict; │ │ 912 - if (target_value_cursor.slot().tag != baseline_value_cursor.slot().tag) { │ │ 913 - return error.UnexpectedTag; │ │ 914 - } │ │ 915 - if (target_value_cursor.slot().value != baseline_value_cursor.slot().value) { │ │ 916 - return error.MergeConflict; │ │ 917 - } │ │ 918 - │ │ 919 - try target.put(key, .{ .slot = kv_pair.value_cursor.slot() }); │ │ 920 - } │ │ 921 - } │ │ 922 -} │ │ 923 - │ │ 924 -// three-way merge of a sorted set: apply the parent's changes relative to the │ │ 925 -// baseline. keys have no values, so additions and removals can't conflict. │ │ 926 -fn mergeChangedSetEntries( │ │ 927 - comptime DB: type, │ │ 928 - allocator: std.mem.Allocator, │ │ 929 - target: DB.SortedSet(.read_write), │ │ 930 - parent: DB.SortedSet(.read_only), │ │ 931 - baseline: DB.SortedSet(.read_only), │ │ 932 -) !void { │ │ 933 - var parent_iter = try parent.iterator(); │ │ 934 - while (try parent_iter.next()) |kv_pair_cursor| { │ │ 935 - const kv_pair = try kv_pair_cursor.readKeyValuePair(); │ │ 936 - const key = try kv_pair.key_cursor.readBytesAlloc(allocator, null); │ │ 937 - defer allocator.free(key); │ │ 938 - if (!try baseline.contains(key)) try target.put(key); │ │ 939 - } │ │ 940 - │ │ 941 - var baseline_iter = try baseline.iterator(); │ │ 942 - while (try baseline_iter.next()) |kv_pair_cursor| { │ │ 943 - const kv_pair = try kv_pair_cursor.readKeyValuePair(); │ │ 944 - const key = try kv_pair.key_cursor.readBytesAlloc(allocator, null); │ │ 945 - defer allocator.free(key); │ │ 946 - if (!try parent.contains(key)) _ = try target.remove(key); │ │ 947 - } │ │ 948 -} │ │ 949 - │ │1134 fn upsertField( │ │1135 comptime DB: type, │ │1136 comptime hash_kind: hash.HashKind, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯