Skip to documentation
SLOP

tiny.sql.merge

Reference tiny.sql merge

Defined in tiny.sql.

API (16)

Actions

Public operations.

Types and contracts

Public types and contracts.

No direct callersNo direct callstiny.sqlmerge
Static calls · unresolved targets: unknown · external targets: unknown.

Source

Called byCallsNo direct callersConflictRootemptymerge.ConflictSnapshotempty
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallstest sourcelib.sql.src.mergetest: database merge applies clean re...test sourcelib.sql.src.mergetest: database merge contains cross-s...test sourcelib.sql.src.mergetest: database merge matches relation...test sourcelib.sql.src.mergetest: database merge preserves a thei...test sourcelib.sql.src.mergetest: database merge records relation...test sourcelib.sql.src.mergetest: database merge supersedes stale...private sourcelib.sql.src.merge.DatabaseRelationMergedeinitprivate sourcelib.sql.src.mergeappendClonedRelationValueprivate sourcelib.sql.src.mergeappendDatabaseConflictprivate sourcelib.sql.src.mergeappendRelationConflictprivate sourcelib.sql.src.mergeappendSnapshotNames+14 moremergemergeDatabase
Static calls · unresolved targets: 3 · external targets: 12.
Called byCallsprivate sourcelib.sql.src.mergemergeExistingRelationtest sourcelib.sql.src.mergetest: database merge contains cross-s...test sourcelib.sql.src.mergetest: relation merge coalesces identi...test sourcelib.sql.src.mergetest: relation merge records divergen...test sourcelib.sql.src.mergetest: relation merge resolves nonover...private sourcelib.sql.src.merge.Conflictdeinitprivate sourcelib.sql.src.merge.Editdeinitprivate sourcelib.sql.src.mergeappendConflictprivate sourcelib.sql.src.mergeappendEditprivate sourcelib.sql.src.mergeentryBytes+4 moremergerelation
Static calls · unresolved targets: 2 · external targets: 7.

Source: lib/sql/src/merge.zig

zig
const std = @import("std");const simd = @import("simd");const catalog_mod = @import("catalog.zig");const diff_mod = @import("diff.zig");const history_mod = @import("history/root.zig");const key = @import("key.zig");const relation_mod = @import("relation.zig");const row = @import("row.zig");const version = @import("version.zig");const wal = @import("wal.zig");const Bytes = simd.ScalableTag(u8);const Allocator = std.mem.Allocator;pub const Error = version.Error || relation_mod.Error || key.Error || row.Error || error{    DuplicateConflictSlot,    InvalidConflictSnapshot,    SchemaMismatch,};pub const RelationSnapshot = diff_mod.RelationSnapshot;pub const ConflictSnapshot = struct {    root: version.ConflictRoot,    artifacts: []const version.ConflictArtifact,    pub fn empty() ConflictSnapshot {        return .{            .root = version.ConflictRoot.empty(),            .artifacts = &.{},        };    }};pub const DatabaseSnapshot = struct {    relations: []const RelationSnapshot,    conflicts: ConflictSnapshot,};pub const DatabaseSource = struct {    value: *const version.DatabaseValue,    conflicts: ConflictSnapshot,};pub const EditKind = enum {    added,    removed,    modified,};pub const Edit = struct {    kind: EditKind,    rowid: i64,    from: ?[]u8 = null,    to: ?[]u8 = null,    fn deinit(self: *Edit, allocator: Allocator) void {        if (self.from) |bytes| allocator.free(bytes);        if (self.to) |bytes| allocator.free(bytes);        self.* = undefined;    }};pub const Conflict = struct {    relation: []u8,    rowid: i64,    base: ?[]u8 = null,    ours: ?[]u8 = null,    theirs: ?[]u8 = null,    artifact: version.ConflictArtifact,    fn deinit(self: *Conflict, allocator: Allocator) void {        allocator.free(self.relation);        if (self.base) |bytes| allocator.free(bytes);        if (self.ours) |bytes| allocator.free(bytes);        if (self.theirs) |bytes| allocator.free(bytes);        self.* = undefined;    }};pub const RelationMerge = struct {    allocator: Allocator,    edits: []Edit,    conflicts: []Conflict,    pub fn hasConflicts(self: *const RelationMerge) bool {        return self.conflicts.len != 0;    }    pub fn deinit(self: *RelationMerge) void {        for (self.edits) |*edit| edit.deinit(self.allocator);        for (self.conflicts) |*conflict| conflict.deinit(self.allocator);        if (self.edits.len != 0) self.allocator.free(self.edits);        if (self.conflicts.len != 0) self.allocator.free(self.conflicts);        self.* = undefined;    }};pub const DatabaseConflict = struct {    kind: version.ConflictKind = .row,    relation: []const u8,    rowid: ?i64 = null,    base: ?[]const u8 = null,    ours: ?[]const u8 = null,    theirs: ?[]const u8 = null,    base_root: ?version.Hash = null,    ours_root: ?version.Hash = null,    theirs_root: ?version.Hash = null,    artifact: version.ConflictArtifact,};pub const RelationMergeMode = enum {    rows,    ours,    theirs,    agreed,    conflict,};pub const DatabaseRelationMerge = struct {    name: []u8,    root: version.Hash,    mode: RelationMergeMode,    result: RelationMerge,    fn deinit(self: *DatabaseRelationMerge, allocator: Allocator) void {        allocator.free(self.name);        self.result.deinit();        self.* = undefined;    }};pub const DatabaseMerge = struct {    allocator: Allocator,    value: version.DatabaseValue,    conflict_root: version.ConflictRoot,    relations: []DatabaseRelationMerge,    discovered: []DatabaseConflict,    artifacts: []version.ConflictArtifact,    pub fn hasConflicts(self: *const DatabaseMerge) bool {        return self.conflict_root.count != 0;    }    pub fn persistConflicts(self: *const DatabaseMerge, history: *history_mod.History) history_mod.Error!void {        for (self.artifacts) |artifact| try history.putConflict(artifact);        const entries = try self.allocator.alloc(version.ConflictEntry, self.artifacts.len);        defer if (entries.len != 0) self.allocator.free(entries);        for (self.artifacts, entries) |artifact, *entry| entry.* = artifact.entry();        const persisted = try history.putConflictRoot(entries);        if (!version.same(persisted.hash, self.conflict_root.hash) or persisted.count != self.conflict_root.count) return error.InvalidHistory;    }    pub fn deinit(self: *DatabaseMerge) void {        self.value.deinit();        for (self.relations) |*entry| entry.deinit(self.allocator);        for (self.artifacts) |*artifact| deinitConflictArtifact(self.allocator, artifact);        if (self.relations.len != 0) self.allocator.free(self.relations);        if (self.discovered.len != 0) self.allocator.free(self.discovered);        if (self.artifacts.len != 0) self.allocator.free(self.artifacts);        self.* = undefined;    }};pub fn relation(allocator: Allocator, base: RelationSnapshot, ours: RelationSnapshot, theirs: RelationSnapshot) Error!RelationMerge {    if (!schemasAgree(base.root, ours.root, theirs.root)) return error.SchemaMismatch;    var edits: std.ArrayList(Edit) = .empty;    errdefer {        for (edits.items) |*edit| edit.deinit(allocator);        edits.deinit(allocator);    }    var conflicts: std.ArrayList(Conflict) = .empty;    errdefer {        for (conflicts.items) |*conflict| conflict.deinit(allocator);        conflicts.deinit(allocator);    }    var base_scan: diff_mod.Scan = undefined;    try base.rows.scan(&base_scan, allocator, null, null);    defer base_scan.deinit();    var ours_scan: diff_mod.Scan = undefined;    try ours.rows.scan(&ours_scan, allocator, null, null);    defer ours_scan.deinit();    var theirs_scan: diff_mod.Scan = undefined;    try theirs.rows.scan(&theirs_scan, allocator, null, null);    defer theirs_scan.deinit();    var base_entry = try base_scan.next();    var ours_entry = try ours_scan.next();    var theirs_entry = try theirs_scan.next();    while (lowestRowid(base_entry, ours_entry, theirs_entry)) |rowid| {        const base_bytes = entryBytes(base_entry, rowid);        const ours_bytes = entryBytes(ours_entry, rowid);        const theirs_bytes = entryBytes(theirs_entry, rowid);        const ours_changed = !sameBytes(base_bytes, ours_bytes);        const theirs_changed = !sameBytes(base_bytes, theirs_bytes);        if (ours_changed or theirs_changed) {            if (!ours_changed) {                try appendEdit(allocator, &edits, rowid, base_bytes, theirs_bytes);            } else if (!theirs_changed) {                try appendEdit(allocator, &edits, rowid, base_bytes, ours_bytes);            } else if (sameBytes(ours_bytes, theirs_bytes)) {                try appendEdit(allocator, &edits, rowid, base_bytes, ours_bytes);            } else {                try appendConflict(allocator, &conflicts, ours.root.name, rowid, base_bytes, ours_bytes, theirs_bytes);            }        }        if (entryMatches(base_entry, rowid)) base_entry = try base_scan.next();        if (entryMatches(ours_entry, rowid)) ours_entry = try ours_scan.next();        if (entryMatches(theirs_entry, rowid)) theirs_entry = try theirs_scan.next();    }    return .{        .allocator = allocator,        .edits = try edits.toOwnedSlice(allocator),        .conflicts = try conflicts.toOwnedSlice(allocator),    };}pub fn mergeDatabase(allocator: Allocator, base: DatabaseSnapshot, ours: DatabaseSource, theirs: DatabaseSnapshot) Error!DatabaseMerge {    try validateConflictSnapshot(allocator, base.conflicts, null);    try validateConflictSnapshot(allocator, ours.conflicts, ours.value.root.conflicts);    try validateConflictSnapshot(allocator, theirs.conflicts, null);    var relation_results: std.ArrayList(DatabaseRelationMerge) = .empty;    errdefer {        for (relation_results.items) |*entry| entry.deinit(allocator);        relation_results.deinit(allocator);    }    var fresh_artifacts: std.ArrayList(version.ConflictArtifact) = .empty;    defer fresh_artifacts.deinit(allocator);    errdefer {        for (fresh_artifacts.items) |*artifact| deinitConflictArtifact(allocator, artifact);    }    var relation_values: std.ArrayList(version.RelationValue) = .empty;    errdefer {        for (relation_values.items) |*value| value.deinit(allocator);        relation_values.deinit(allocator);    }    var names: std.ArrayList([]const u8) = .empty;    defer names.deinit(allocator);    try appendSnapshotNames(allocator, &names, base.relations);    try appendValueNames(allocator, &names, ours.value.relations);    try appendSnapshotNames(allocator, &names, theirs.relations);    std.mem.sort([]const u8, names.items, {}, relationNameLessThan);    for (names.items) |name| {        const base_relation = findSnapshot(base.relations, name);        const ours_relation = ours.value.findRelation(name);        const theirs_relation = findSnapshot(theirs.relations, name);        if (base_relation == null) {            if (ours_relation) |ours_value| {                if (theirs_relation) |theirs_snapshot| {                    if (!version.same(ours_value.root.hash, theirs_snapshot.root.hash)) {                        try appendRelationConflict(allocator, &fresh_artifacts, name, null, ours_value.root.hash, theirs_snapshot.root.hash);                    }                }                try appendClonedRelationValue(allocator, &relation_values, ours_value);            } else if (theirs_relation) |theirs_snapshot| {                try appendSnapshotRelationValue(allocator, &relation_values, theirs_snapshot);            }            continue;        }        const base_snapshot = base_relation.?;        if (ours_relation == null and theirs_relation == null) continue;        if (ours_relation == null) {            const theirs_snapshot = theirs_relation orelse unreachable;            if (!version.same(base_snapshot.root.hash, theirs_snapshot.root.hash)) {                try appendRelationConflict(allocator, &fresh_artifacts, name, base_snapshot.root.hash, null, theirs_snapshot.root.hash);            }            continue;        }        if (theirs_relation == null) {            const ours_value = ours_relation.?;            if (!version.same(base_snapshot.root.hash, ours_value.root.hash)) {                try appendRelationConflict(allocator, &fresh_artifacts, name, base_snapshot.root.hash, ours_value.root.hash, null);                try appendClonedRelationValue(allocator, &relation_values, ours_value);            }            continue;        }        var existing = try mergeExistingRelation(            allocator,            name,            base_snapshot,            ours_relation.?,            theirs_relation.?,        );        var entry_owned = true;        errdefer if (entry_owned) existing.entry.deinit(allocator);        var value_owned = true;        errdefer if (value_owned) existing.value.deinit(allocator);        if (existing.relation_conflict) {            try appendRelationConflict(                allocator,                &fresh_artifacts,                name,                base_snapshot.root.hash,                ours_relation.?.root.hash,                theirs_relation.?.root.hash,            );        }        for (existing.entry.result.conflicts) |conflict| {            try appendDatabaseConflict(allocator, &fresh_artifacts, conflict);        }        try relation_values.append(allocator, existing.value);        value_owned = false;        try relation_results.append(allocator, existing.entry);        entry_owned = false;    }    const artifact_slice = try reconcileConflictArtifacts(        allocator,        base.conflicts,        ours.conflicts,        theirs.conflicts,        &fresh_artifacts,    );    errdefer {        for (artifact_slice) |*artifact| deinitConflictArtifact(allocator, artifact);        if (artifact_slice.len != 0) allocator.free(artifact_slice);    }    const discovered_slice = try allocator.alloc(DatabaseConflict, fresh_artifacts.items.len);    errdefer if (discovered_slice.len != 0) allocator.free(discovered_slice);    for (fresh_artifacts.items, discovered_slice) |artifact, *conflict| {        const retained = findConflictArtifact(artifact_slice, artifact.entry()) orelse return error.InvalidConflictSnapshot;        conflict.* = databaseConflict(retained.*);    }    for (fresh_artifacts.items) |*artifact| deinitConflictArtifact(allocator, artifact);    fresh_artifacts.clearRetainingCapacity();    const conflict_entries = try allocator.alloc(version.ConflictEntry, artifact_slice.len);    defer if (conflict_entries.len != 0) allocator.free(conflict_entries);    for (artifact_slice, conflict_entries) |artifact, *entry| entry.* = artifact.entry();    const conflict_root = if (artifact_slice.len == 0) version.ConflictRoot.empty() else version.ConflictRoot.init(conflict_entries);    const relation_value_slice = try relation_values.toOwnedSlice(allocator);    var relation_value_slice_owned = true;    errdefer {        if (relation_value_slice_owned) {            for (relation_value_slice) |*value| value.deinit(allocator);            if (relation_value_slice.len != 0) allocator.free(relation_value_slice);        }    }    var value = try version.databaseValueFromOwnedRelations(allocator, relation_value_slice, conflict_root.hash);    relation_value_slice_owned = false;    errdefer value.deinit();    const relation_slice = try relation_results.toOwnedSlice(allocator);    errdefer {        for (relation_slice) |*entry| entry.deinit(allocator);        if (relation_slice.len != 0) allocator.free(relation_slice);    }    return .{        .allocator = allocator,        .value = value,        .conflict_root = conflict_root,        .relations = relation_slice,        .discovered = discovered_slice,        .artifacts = artifact_slice,    };}fn validateConflictSnapshot(allocator: Allocator, snapshot: ConflictSnapshot, expected: ?version.Hash) Error!void {    if (snapshot.root.count != snapshot.artifacts.len) return error.InvalidConflictSnapshot;    if (expected) |hash| {        if (!version.same(hash, snapshot.root.hash)) return error.InvalidConflictSnapshot;    }    const entries = try allocator.alloc(version.ConflictEntry, snapshot.artifacts.len);    defer if (entries.len != 0) allocator.free(entries);    for (snapshot.artifacts, entries) |artifact, *entry| {        try validateConflictArtifact(artifact);        entry.* = artifact.entry();    }    std.mem.sort(version.ConflictEntry, entries, {}, version.ConflictEntry.lessThan);    const derived = if (entries.len == 0) version.ConflictRoot.empty() else version.ConflictRoot.init(entries);    if (!version.same(derived.hash, snapshot.root.hash) or derived.count != snapshot.root.count) return error.InvalidConflictSnapshot;}fn validateConflictArtifact(artifact: version.ConflictArtifact) Error!void {    const canonical = switch (artifact.kind) {        .row => version.ConflictArtifact.init(            artifact.relation,            artifact.rowid,            try optionalRowValue(artifact.base),            try optionalRowValue(artifact.ours),            try optionalRowValue(artifact.theirs),        ),        .relation => blk: {            if (artifact.rowid != 0) return error.InvalidConflictSnapshot;            break :blk version.ConflictArtifact.initRelation(                artifact.relation,                try optionalRelationValue(artifact.base),                try optionalRelationValue(artifact.ours),                try optionalRelationValue(artifact.theirs),            );        },    };    if (!version.same(canonical.hash, artifact.hash)) return error.InvalidConflictSnapshot;}fn reconcileConflictArtifacts(    allocator: Allocator,    base: ConflictSnapshot,    ours: ConflictSnapshot,    theirs: ConflictSnapshot,    fresh: *const std.ArrayList(version.ConflictArtifact),) Error![]version.ConflictArtifact {    for (fresh.items, 0..) |artifact, index| {        try validateConflictArtifact(artifact);        for (fresh.items[0..index]) |previous| {            if (artifact.entry().sameSlot(previous.entry())) return error.DuplicateConflictSlot;        }    }    var capacity = std.math.add(usize, base.artifacts.len, ours.artifacts.len) catch return error.InvalidConflictSnapshot;    capacity = std.math.add(usize, capacity, theirs.artifacts.len) catch return error.InvalidConflictSnapshot;    capacity = std.math.add(usize, capacity, fresh.items.len) catch return error.InvalidConflictSnapshot;    var result: std.ArrayList(version.ConflictArtifact) = .empty;    errdefer {        for (result.items) |*artifact| deinitConflictArtifact(allocator, artifact);        result.deinit(allocator);    }    try result.ensureTotalCapacity(allocator, capacity);    for (base.artifacts) |artifact| try appendCarriedArtifact(allocator, &result, artifact, base, ours, theirs, fresh.items);    for (ours.artifacts) |artifact| try appendCarriedArtifact(allocator, &result, artifact, base, ours, theirs, fresh.items);    for (theirs.artifacts) |artifact| try appendCarriedArtifact(allocator, &result, artifact, base, ours, theirs, fresh.items);    for (fresh.items) |artifact| try appendUniqueConflictArtifact(allocator, &result, artifact);    std.mem.sort(version.ConflictArtifact, result.items, {}, conflictArtifactLessThan);    return try result.toOwnedSlice(allocator);}fn appendCarriedArtifact(    allocator: Allocator,    result: *std.ArrayList(version.ConflictArtifact),    artifact: version.ConflictArtifact,    base: ConflictSnapshot,    ours: ConflictSnapshot,    theirs: ConflictSnapshot,    fresh: []const version.ConflictArtifact,) Error!void {    const entry = artifact.entry();    const in_base = hasConflictArtifact(base.artifacts, entry);    const in_ours = hasConflictArtifact(ours.artifacts, entry);    const in_theirs = hasConflictArtifact(theirs.artifacts, entry);    const keep = if (in_base) in_ours and in_theirs else in_ours or in_theirs;    if (!keep or hasConflictSlot(fresh, entry)) return;    try appendUniqueConflictArtifact(allocator, result, artifact);}fn appendUniqueConflictArtifact(allocator: Allocator, result: *std.ArrayList(version.ConflictArtifact), artifact: version.ConflictArtifact) Error!void {    if (findConflictArtifact(result.items, artifact.entry()) != null) return;    result.appendAssumeCapacity(try cloneConflictArtifact(allocator, artifact));}fn hasConflictArtifact(artifacts: []const version.ConflictArtifact, entry: version.ConflictEntry) bool {    return findConflictArtifact(artifacts, entry) != null;}fn findConflictArtifact(artifacts: []const version.ConflictArtifact, entry: version.ConflictEntry) ?*const version.ConflictArtifact {    for (artifacts) |*artifact| {        if (artifact.entry().eql(entry)) return artifact;    }    return null;}fn hasConflictSlot(artifacts: []const version.ConflictArtifact, entry: version.ConflictEntry) bool {    for (artifacts) |artifact| {        if (artifact.entry().sameSlot(entry)) return true;    }    return false;}fn cloneConflictArtifact(allocator: Allocator, artifact: version.ConflictArtifact) Error!version.ConflictArtifact {    const relation_name = try allocator.dupe(u8, artifact.relation);    errdefer allocator.free(relation_name);    return switch (artifact.kind) {        .row => blk: {            const base = try copyOptional(allocator, try optionalRowValue(artifact.base));            errdefer if (base) |bytes| allocator.free(bytes);            const ours = try copyOptional(allocator, try optionalRowValue(artifact.ours));            errdefer if (ours) |bytes| allocator.free(bytes);            const theirs = try copyOptional(allocator, try optionalRowValue(artifact.theirs));            errdefer if (theirs) |bytes| allocator.free(bytes);            break :blk version.ConflictArtifact.init(relation_name, artifact.rowid, base, ours, theirs);        },        .relation => version.ConflictArtifact.initRelation(            relation_name,            try optionalRelationValue(artifact.base),            try optionalRelationValue(artifact.ours),            try optionalRelationValue(artifact.theirs),        ),    };}fn deinitConflictArtifact(allocator: Allocator, artifact: *version.ConflictArtifact) void {    allocator.free(artifact.relation);    if (artifact.kind == .row) {        if (artifact.base) |value| allocator.free(value.row);        if (artifact.ours) |value| allocator.free(value.row);        if (artifact.theirs) |value| allocator.free(value.row);    }    artifact.* = undefined;}fn optionalRowValue(value: ?version.ConflictValue) Error!?[]const u8 {    const present = value orelse return null;    return switch (present) {        .row => |bytes| bytes,        .relation => error.InvalidConflictSnapshot,    };}fn optionalRelationValue(value: ?version.ConflictValue) Error!?version.Hash {    const present = value orelse return null;    return switch (present) {        .row => error.InvalidConflictSnapshot,        .relation => |hash| hash,    };}fn conflictArtifactLessThan(_: void, left: version.ConflictArtifact, right: version.ConflictArtifact) bool {    return version.ConflictEntry.lessThan({}, left.entry(), right.entry());}fn databaseConflict(artifact: version.ConflictArtifact) DatabaseConflict {    return switch (artifact.kind) {        .row => .{            .kind = .row,            .relation = artifact.relation,            .rowid = artifact.rowid,            .base = optionalRowValue(artifact.base) catch unreachable,            .ours = optionalRowValue(artifact.ours) catch unreachable,            .theirs = optionalRowValue(artifact.theirs) catch unreachable,            .artifact = artifact,        },        .relation => .{            .kind = .relation,            .relation = artifact.relation,            .base_root = optionalRelationValue(artifact.base) catch unreachable,            .ours_root = optionalRelationValue(artifact.ours) catch unreachable,            .theirs_root = optionalRelationValue(artifact.theirs) catch unreachable,            .artifact = artifact,        },    };}fn appendSnapshotNames(allocator: Allocator, names: *std.ArrayList([]const u8), relations: []const RelationSnapshot) Allocator.Error!void {    for (relations) |entry| try appendRelationName(allocator, names, entry.root.name);}fn appendValueNames(allocator: Allocator, names: *std.ArrayList([]const u8), relations: []const version.RelationValue) Allocator.Error!void {    for (relations) |entry| try appendRelationName(allocator, names, entry.root.name);}fn appendRelationName(allocator: Allocator, names: *std.ArrayList([]const u8), name: []const u8) Allocator.Error!void {    for (names.items) |existing| {        if (std.mem.eql(u8, existing, name)) return;    }    try names.append(allocator, name);}fn relationNameLessThan(_: void, left: []const u8, right: []const u8) bool {    return simd.order(Bytes, left, right) == .lt;}fn findSnapshot(relations: []const RelationSnapshot, name: []const u8) ?*const RelationSnapshot {    for (relations) |*entry| {        if (std.mem.eql(u8, entry.root.name, name)) return entry;    }    return null;}const ExistingRelationMerge = struct {    value: version.RelationValue,    entry: DatabaseRelationMerge,    relation_conflict: bool,};fn mergeExistingRelation(    allocator: Allocator,    name: []const u8,    base: *const RelationSnapshot,    ours: *const version.RelationValue,    theirs: *const RelationSnapshot,) Error!ExistingRelationMerge {    if (schemasAgree(base.root, &ours.root, theirs.root)) {        const ours_snapshot = RelationSnapshot{            .root = &ours.root,            .rows = .{ .materialized = ours.rows },        };        var merged = try relation(allocator, base.*, ours_snapshot, theirs.*);        errdefer merged.deinit();        var value = try relationValueApplyingMerge(allocator, ours, merged.edits);        errdefer value.deinit(allocator);        return .{            .value = value,            .entry = .{                .name = try allocator.dupe(u8, name),                .root = value.root.hash,                .mode = .rows,                .result = merged,            },            .relation_conflict = false,        };    }    const mode = wholeRelationMode(base.root, &ours.root, theirs.root);    var value = try wholeRelationValue(allocator, mode, ours, theirs);    errdefer value.deinit(allocator);    return .{        .value = value,        .entry = .{            .name = try allocator.dupe(u8, name),            .root = value.root.hash,            .mode = mode,            .result = emptyRelationMerge(allocator),        },        .relation_conflict = mode == .conflict,    };}fn schemasAgree(    base: *const version.RelationRoot,    ours: *const version.RelationRoot,    theirs: *const version.RelationRoot,) bool {    return version.same(base.schema, ours.schema) and        version.same(ours.schema, theirs.schema);}fn wholeRelationMode(    base: *const version.RelationRoot,    ours: *const version.RelationRoot,    theirs: *const version.RelationRoot,) RelationMergeMode {    if (version.same(ours.hash, theirs.hash)) return .agreed;    if (version.same(ours.hash, base.hash)) return .theirs;    if (version.same(theirs.hash, base.hash)) return .ours;    return .conflict;}fn wholeRelationValue(    allocator: Allocator,    mode: RelationMergeMode,    ours: *const version.RelationValue,    theirs: *const RelationSnapshot,) Error!version.RelationValue {    return switch (mode) {        .theirs => try relationValueFromSnapshot(allocator, theirs),        .ours, .agreed, .conflict => try ours.clone(allocator),        .rows => unreachable,    };}fn emptyRelationMerge(allocator: Allocator) RelationMerge {    return .{        .allocator = allocator,        .edits = &.{},        .conflicts = &.{},    };}fn appendClonedRelationValue(allocator: Allocator, relation_values: *std.ArrayList(version.RelationValue), source: *const version.RelationValue) Allocator.Error!void {    var clone = try source.clone(allocator);    var clone_owned = true;    errdefer if (clone_owned) clone.deinit(allocator);    try relation_values.append(allocator, clone);    clone_owned = false;}fn appendSnapshotRelationValue(allocator: Allocator, relation_values: *std.ArrayList(version.RelationValue), source: *const RelationSnapshot) Error!void {    var value = try relationValueFromSnapshot(allocator, source);    var value_owned = true;    errdefer if (value_owned) value.deinit(allocator);    try relation_values.append(allocator, value);    value_owned = false;}fn relationValueFromSnapshot(allocator: Allocator, snapshot: *const RelationSnapshot) Error!version.RelationValue {    var root = try snapshot.root.clone(allocator);    errdefer root.deinit();    var rows: std.ArrayList(version.RelationRow) = .empty;    errdefer {        for (rows.items) |row_value| allocator.free(row_value.bytes);        rows.deinit(allocator);    }    var scan: diff_mod.Scan = undefined;    try snapshot.rows.scan(&scan, allocator, null, null);    defer scan.deinit();    while (try scan.next()) |entry| {        const bytes = try allocator.dupe(u8, entry.bytes);        rows.append(allocator, .{            .rowid = entry.rowid,            .bytes = bytes,        }) catch |err| {            allocator.free(bytes);            return err;        };    }    return .{        .root = root,        .rows = try rows.toOwnedSlice(allocator),    };}fn appendEdit(allocator: Allocator, edits: *std.ArrayList(Edit), rowid: i64, from_bytes: ?[]const u8, to_bytes: ?[]const u8) Error!void {    if (sameBytes(from_bytes, to_bytes)) return;    const from = try copyOptional(allocator, from_bytes);    errdefer if (from) |bytes| allocator.free(bytes);    const to = try copyOptional(allocator, to_bytes);    errdefer if (to) |bytes| allocator.free(bytes);    try edits.append(allocator, .{        .kind = editKind(from_bytes, to_bytes),        .rowid = rowid,        .from = from,        .to = to,    });}fn appendConflict(allocator: Allocator, conflicts: *std.ArrayList(Conflict), relation_name: []const u8, rowid: i64, base_bytes: ?[]const u8, ours_bytes: ?[]const u8, theirs_bytes: ?[]const u8) Error!void {    const owned_relation = try allocator.dupe(u8, relation_name);    errdefer allocator.free(owned_relation);    const base = try copyOptional(allocator, base_bytes);    errdefer if (base) |bytes| allocator.free(bytes);    const ours = try copyOptional(allocator, ours_bytes);    errdefer if (ours) |bytes| allocator.free(bytes);    const theirs = try copyOptional(allocator, theirs_bytes);    errdefer if (theirs) |bytes| allocator.free(bytes);    const artifact = version.ConflictArtifact.init(owned_relation, rowid, base, ours, theirs);    try conflicts.append(allocator, .{        .relation = owned_relation,        .rowid = rowid,        .base = base,        .ours = ours,        .theirs = theirs,        .artifact = artifact,    });}fn appendDatabaseConflict(allocator: Allocator, conflicts: *std.ArrayList(version.ConflictArtifact), conflict: Conflict) Error!void {    const relation_name = try allocator.dupe(u8, conflict.artifact.relation);    errdefer allocator.free(relation_name);    const base = try copyOptional(allocator, conflict.base);    errdefer if (base) |bytes| allocator.free(bytes);    const ours = try copyOptional(allocator, conflict.ours);    errdefer if (ours) |bytes| allocator.free(bytes);    const theirs = try copyOptional(allocator, conflict.theirs);    errdefer if (theirs) |bytes| allocator.free(bytes);    const artifact = version.ConflictArtifact.init(relation_name, conflict.artifact.rowid, base, ours, theirs);    try conflicts.append(allocator, artifact);}fn appendRelationConflict(allocator: Allocator, conflicts: *std.ArrayList(version.ConflictArtifact), relation_name: []const u8, base: ?version.Hash, ours: ?version.Hash, theirs: ?version.Hash) Error!void {    const owned_relation = try allocator.dupe(u8, relation_name);    errdefer allocator.free(owned_relation);    const artifact = version.ConflictArtifact.initRelation(owned_relation, base, ours, theirs);    try conflicts.append(allocator, artifact);}fn relationValueApplyingMerge(allocator: Allocator, ours: *const version.RelationValue, edits: []const Edit) Error!version.RelationValue {    var relation_edits: std.ArrayList(relation_mod.Edit) = .empty;    defer relation_edits.deinit(allocator);    try relation_edits.ensureTotalCapacity(allocator, edits.len);    var ours_index: usize = 0;    var previous_rowid: ?i64 = null;    for (edits) |edit| {        if (previous_rowid) |rowid| std.debug.assert(rowid < edit.rowid);        while (ours_index < ours.rows.len and ours.rows[ours_index].rowid < edit.rowid) {            ours_index += 1;        }        const ours_bytes = if (ours_index < ours.rows.len and            ours.rows[ours_index].rowid == edit.rowid)            ours.rows[ours_index].bytes        else            null;        if (!sameBytes(ours_bytes, edit.to)) {            relation_edits.appendAssumeCapacity(relationEdit(edit));        }        previous_rowid = edit.rowid;    }    return try version.relationValueApplyingMaterializedEdits(allocator, ours, relation_edits.items);}fn relationEdit(edit: Edit) relation_mod.Edit {    if (edit.to) |bytes| {        return .{ .put = .{            .rowid = edit.rowid,            .bytes = bytes,        } };    }    return .{ .delete = edit.rowid };}fn editKind(from_bytes: ?[]const u8, to_bytes: ?[]const u8) EditKind {    if (from_bytes == null) return .added;    if (to_bytes == null) return .removed;    return .modified;}fn copyOptional(allocator: Allocator, bytes: ?[]const u8) Allocator.Error!?[]u8 {    return if (bytes) |value| try allocator.dupe(u8, value) else null;}fn sameBytes(left: ?[]const u8, right: ?[]const u8) bool {    if (left == null and right == null) return true;    if (left == null or right == null) return false;    return std.mem.eql(u8, left.?, right.?);}fn entryBytes(entry: ?diff_mod.Row, rowid: i64) ?[]const u8 {    if (entry) |value| {        if (value.rowid == rowid) return value.bytes;    }    return null;}fn entryMatches(entry: ?diff_mod.Row, rowid: i64) bool {    return if (entry) |value| value.rowid == rowid else false;}fn lowestRowid(base: ?diff_mod.Row, ours: ?diff_mod.Row, theirs: ?diff_mod.Row) ?i64 {    var found: ?i64 = null;    if (base) |entry| found = minRowid(found, entry.rowid);    if (ours) |entry| found = minRowid(found, entry.rowid);    if (theirs) |entry| found = minRowid(found, entry.rowid);    return found;}fn minRowid(current: ?i64, rowid: i64) i64 {    return if (current) |value| @min(value, rowid) else rowid;}test "relation merge resolves nonoverlapping row edits" {    var tmp = std.testing.tmpDir(.{});    defer tmp.cleanup();    var database = try @import("file.zig").Database.openForTesting(std.testing.allocator, tmp.dir, .{        .paths = .{ .database = "merge.db", .wal = "merge.wal" },        .header = testingHeader(),    });    defer database.deinit();    try database.reserve(.{ .wal_frames = 768 });    var catalog = try catalog_mod.Catalog.open(&database, .{});    _ = try catalog.createRelation(std.testing.allocator, .{        .name = "base",        .columns = &.{.{ .name = "value" }},    }, .{ .durability = .buffered });    _ = try catalog.createRelation(std.testing.allocator, .{        .name = "ours",        .columns = &.{.{ .name = "value" }},    }, .{ .durability = .buffered });    _ = try catalog.createRelation(std.testing.allocator, .{        .name = "theirs",        .columns = &.{.{ .name = "value" }},    }, .{ .durability = .buffered });    var base = try catalog.openRelation(std.testing.allocator, "base");    defer base.deinit();    var ours = try catalog.openRelation(std.testing.allocator, "ours");    defer ours.deinit();    var theirs = try catalog.openRelation(std.testing.allocator, "theirs");    defer theirs.deinit();    _ = try base.relation.put(std.testing.allocator, 1, &.{.{ .text = "same" }}, .{ .durability = .buffered });    _ = try base.relation.put(std.testing.allocator, 2, &.{.{ .text = "base" }}, .{ .durability = .buffered });    _ = try base.relation.put(std.testing.allocator, 3, &.{.{ .text = "delete" }}, .{ .durability = .buffered });    _ = try ours.relation.put(std.testing.allocator, 1, &.{.{ .text = "same" }}, .{ .durability = .buffered });    _ = try ours.relation.put(std.testing.allocator, 2, &.{.{ .text = "ours" }}, .{ .durability = .buffered });    _ = try ours.relation.put(std.testing.allocator, 3, &.{.{ .text = "delete" }}, .{ .durability = .buffered });    _ = try ours.relation.put(std.testing.allocator, 5, &.{.{ .text = "ours-add" }}, .{ .durability = .buffered });    _ = try theirs.relation.put(std.testing.allocator, 1, &.{.{ .text = "same" }}, .{ .durability = .buffered });    _ = try theirs.relation.put(std.testing.allocator, 2, &.{.{ .text = "base" }}, .{ .durability = .buffered });    _ = try theirs.relation.put(std.testing.allocator, 6, &.{.{ .text = "theirs-add" }}, .{ .durability = .buffered });    const schema = try catalog.schemaState(std.testing.allocator);    var base_root = try version.relationRoot(std.testing.allocator, "base", schema, &base, null);    defer base_root.deinit();    var ours_root = try version.relationRoot(std.testing.allocator, "ours", schema, &ours, null);    defer ours_root.deinit();    var theirs_root = try version.relationRoot(std.testing.allocator, "theirs", schema, &theirs, null);    defer theirs_root.deinit();    var result = try relation(std.testing.allocator, .{        .root = &base_root,        .rows = .{ .live = &base },    }, .{        .root = &ours_root,        .rows = .{ .live = &ours },    }, .{        .root = &theirs_root,        .rows = .{ .live = &theirs },    });    defer result.deinit();    try std.testing.expect(!result.hasConflicts());    try std.testing.expectEqual(@as(usize, 4), result.edits.len);    try std.testing.expectEqual(EditKind.modified, result.edits[0].kind);    try std.testing.expectEqual(@as(i64, 2), result.edits[0].rowid);    try std.testing.expectEqual(EditKind.removed, result.edits[1].kind);    try std.testing.expectEqual(@as(i64, 3), result.edits[1].rowid);    try std.testing.expectEqual(EditKind.added, result.edits[2].kind);    try std.testing.expectEqual(@as(i64, 5), result.edits[2].rowid);    try std.testing.expectEqual(EditKind.added, result.edits[3].kind);    try std.testing.expectEqual(@as(i64, 6), result.edits[3].rowid);}test "relation merge records divergent same row conflicts" {    var tmp = std.testing.tmpDir(.{});    defer tmp.cleanup();    var database = try @import("file.zig").Database.openForTesting(std.testing.allocator, tmp.dir, .{        .paths = .{ .database = "merge-conflict.db", .wal = "merge-conflict.wal" },        .header = testingHeader(),    });    defer database.deinit();    try database.reserve(.{ .wal_frames = 768 });    var catalog = try catalog_mod.Catalog.open(&database, .{});    _ = try catalog.createRelation(std.testing.allocator, .{        .name = "base",        .columns = &.{.{ .name = "value" }},    }, .{ .durability = .buffered });    _ = try catalog.createRelation(std.testing.allocator, .{        .name = "ours",        .columns = &.{.{ .name = "value" }},    }, .{ .durability = .buffered });    _ = try catalog.createRelation(std.testing.allocator, .{        .name = "theirs",        .columns = &.{.{ .name = "value" }},    }, .{ .durability = .buffered });    var base = try catalog.openRelation(std.testing.allocator, "base");    defer base.deinit();    var ours = try catalog.openRelation(std.testing.allocator, "ours");    defer ours.deinit();    var theirs = try catalog.openRelation(std.testing.allocator, "theirs");    defer theirs.deinit();    _ = try base.relation.put(std.testing.allocator, 1, &.{.{ .text = "base" }}, .{ .durability = .buffered });    _ = try ours.relation.put(std.testing.allocator, 1, &.{.{ .text = "ours" }}, .{ .durability = .buffered });    _ = try theirs.relation.put(std.testing.allocator, 1, &.{.{ .text = "theirs" }}, .{ .durability = .buffered });    const schema = try catalog.schemaState(std.testing.allocator);    var base_root = try version.relationRoot(std.testing.allocator, "base", schema, &base, null);    defer base_root.deinit();    var ours_root = try version.relationRoot(std.testing.allocator, "ours", schema, &ours, null);    defer ours_root.deinit();    var theirs_root = try version.relationRoot(std.testing.allocator, "theirs", schema, &theirs, null);    defer theirs_root.deinit();    var result = try relation(std.testing.allocator, .{        .root = &base_root,        .rows = .{ .live = &base },    }, .{        .root = &ours_root,        .rows = .{ .live = &ours },    }, .{        .root = &theirs_root,        .rows = .{ .live = &theirs },    });    defer result.deinit();    try std.testing.expect(result.hasConflicts());    try std.testing.expectEqual(@as(usize, 0), result.edits.len);    try std.testing.expectEqual(@as(usize, 1), result.conflicts.len);    try std.testing.expectEqualStrings("ours", result.conflicts[0].relation);    try std.testing.expectEqual(@as(i64, 1), result.conflicts[0].rowid);    const expected = version.ConflictArtifact.init("ours", 1, result.conflicts[0].base, result.conflicts[0].ours, result.conflicts[0].theirs);    try std.testing.expect(version.same(expected.hash, result.conflicts[0].artifact.hash));}test "relation merge coalesces identical concurrent edits" {    var tmp = std.testing.tmpDir(.{});    defer tmp.cleanup();    var database = try @import("file.zig").Database.openForTesting(std.testing.allocator, tmp.dir, .{        .paths = .{ .database = "merge-same.db", .wal = "merge-same.wal" },        .header = testingHeader(),    });    defer database.deinit();    try database.reserve(.{ .wal_frames = 768 });    var catalog = try catalog_mod.Catalog.open(&database, .{});    _ = try catalog.createRelation(std.testing.allocator, .{        .name = "base",        .columns = &.{.{ .name = "value" }},    }, .{ .durability = .buffered });    _ = try catalog.createRelation(std.testing.allocator, .{        .name = "ours",        .columns = &.{.{ .name = "value" }},    }, .{ .durability = .buffered });    _ = try catalog.createRelation(std.testing.allocator, .{        .name = "theirs",        .columns = &.{.{ .name = "value" }},    }, .{ .durability = .buffered });    var base = try catalog.openRelation(std.testing.allocator, "base");    defer base.deinit();    var ours = try catalog.openRelation(std.testing.allocator, "ours");    defer ours.deinit();    var theirs = try catalog.openRelation(std.testing.allocator, "theirs");    defer theirs.deinit();    _ = try base.relation.put(std.testing.allocator, 1, &.{.{ .text = "base" }}, .{ .durability = .buffered });    _ = try ours.relation.put(std.testing.allocator, 1, &.{.{ .text = "same" }}, .{ .durability = .buffered });    _ = try theirs.relation.put(std.testing.allocator, 1, &.{.{ .text = "same" }}, .{ .durability = .buffered });    const schema = try catalog.schemaState(std.testing.allocator);    var base_root = try version.relationRoot(std.testing.allocator, "base", schema, &base, null);    defer base_root.deinit();    var ours_root = try version.relationRoot(std.testing.allocator, "ours", schema, &ours, null);    defer ours_root.deinit();    var theirs_root = try version.relationRoot(std.testing.allocator, "theirs", schema, &theirs, null);    defer theirs_root.deinit();    var result = try relation(std.testing.allocator, .{        .root = &base_root,        .rows = .{ .live = &base },    }, .{        .root = &ours_root,        .rows = .{ .live = &ours },    }, .{        .root = &theirs_root,        .rows = .{ .live = &theirs },    });    defer result.deinit();    try std.testing.expect(!result.hasConflicts());    try std.testing.expectEqual(@as(usize, 1), result.edits.len);    try std.testing.expectEqual(EditKind.modified, result.edits[0].kind);    try std.testing.expectEqual(@as(i64, 1), result.edits[0].rowid);}test "database merge applies clean relation edits and derives root" {    var tmp = std.testing.tmpDir(.{});    defer tmp.cleanup();    var backing = try @import("file.zig").Database.openForTesting(std.testing.allocator, tmp.dir, .{        .paths = .{ .database = "database-merge.db", .wal = "database-merge.wal" },        .header = testingHeader(),    });    defer backing.deinit();    try backing.reserve(.{ .wal_frames = 1024 });    var catalog = try catalog_mod.Catalog.open(&backing, .{});    _ = try catalog.createRelation(std.testing.allocator, .{        .name = "base",        .columns = &.{.{ .name = "value" }},    }, .{ .durability = .buffered });    _ = try catalog.createRelation(std.testing.allocator, .{        .name = "ours",        .columns = &.{.{ .name = "value" }},    }, .{ .durability = .buffered });    _ = try catalog.createRelation(std.testing.allocator, .{        .name = "theirs",        .columns = &.{.{ .name = "value" }},    }, .{ .durability = .buffered });    var base = try catalog.openRelation(std.testing.allocator, "base");    defer base.deinit();    var ours = try catalog.openRelation(std.testing.allocator, "ours");    defer ours.deinit();    var theirs = try catalog.openRelation(std.testing.allocator, "theirs");    defer theirs.deinit();    _ = try base.relation.put(std.testing.allocator, 1, &.{.{ .text = "same" }}, .{ .durability = .buffered });    _ = try base.relation.put(std.testing.allocator, 2, &.{.{ .text = "base" }}, .{ .durability = .buffered });    _ = try base.relation.put(std.testing.allocator, 3, &.{.{ .text = "delete" }}, .{ .durability = .buffered });    _ = try ours.relation.put(std.testing.allocator, 1, &.{.{ .text = "same" }}, .{ .durability = .buffered });    _ = try ours.relation.put(std.testing.allocator, 2, &.{.{ .text = "ours" }}, .{ .durability = .buffered });    _ = try ours.relation.put(std.testing.allocator, 3, &.{.{ .text = "delete" }}, .{ .durability = .buffered });    _ = try ours.relation.put(std.testing.allocator, 5, &.{.{ .text = "ours-add" }}, .{ .durability = .buffered });    _ = try theirs.relation.put(std.testing.allocator, 1, &.{.{ .text = "same" }}, .{ .durability = .buffered });    _ = try theirs.relation.put(std.testing.allocator, 2, &.{.{ .text = "base" }}, .{ .durability = .buffered });    _ = try theirs.relation.put(std.testing.allocator, 6, &.{.{ .text = "theirs-add" }}, .{ .durability = .buffered });    const schema = try catalog.schemaState(std.testing.allocator);    var base_root = try version.relationRoot(std.testing.allocator, "items", schema, &base, null);    defer base_root.deinit();    var theirs_root = try version.relationRoot(std.testing.allocator, "items", schema, &theirs, null);    defer theirs_root.deinit();    var ours_value = try version.relationValue(std.testing.allocator, "items", schema, &ours, null);    defer ours_value.deinit(std.testing.allocator);    var ours_database = try databaseValueForRelation(std.testing.allocator, &ours_value);    defer ours_database.deinit();    var result = try mergeDatabase(std.testing.allocator, .{        .relations = &.{.{ .root = &base_root, .rows = .{ .live = &base } }},        .conflicts = .empty(),    }, .{ .value = &ours_database, .conflicts = .empty() }, .{        .relations = &.{.{ .root = &theirs_root, .rows = .{ .live = &theirs } }},        .conflicts = .empty(),    });    defer result.deinit();    try std.testing.expect(!result.hasConflicts());    try std.testing.expectEqual(@as(usize, 1), result.relations.len);    try std.testing.expectEqual(@as(usize, 4), result.relations[0].result.edits.len);    try std.testing.expectEqual(@as(usize, 0), result.discovered.len);    try std.testing.expectEqual(@as(usize, 0), result.conflict_root.count);    try std.testing.expect(version.same(result.value.root.conflicts, result.conflict_root.hash));    try std.testing.expectEqual(@as(usize, 1), result.value.relations.len);    try std.testing.expectEqual(@as(usize, 4), result.value.relations[0].rows.len);    const live_added = try ours.relation.get(std.testing.allocator, 6);    if (live_added) |bytes| std.testing.allocator.free(bytes);    try std.testing.expect(live_added == null);    const live_removed = (try ours.relation.get(std.testing.allocator, 3)).?;    std.testing.allocator.free(live_removed);    const added = relationRowBytes(result.value.relations[0].rows, 6).?;    const added_view = try row.View.init(added);    try std.testing.expectEqualStrings("theirs-add", (try added_view.column(0)).text);    try std.testing.expect(relationRowBytes(result.value.relations[0].rows, 3) == null);    var expected = try version.DatabaseRoot.initSorted(std.testing.allocator, &.{.{        .name = "items",        .hash = result.relations[0].root,    }}, version.ConflictRoot.empty());    defer expected.deinit();    try std.testing.expect(version.same(expected.hash, result.value.root.hash));}test "database merge supersedes stale slots and persists complete conflict root" {    var tmp = std.testing.tmpDir(.{});    defer tmp.cleanup();    var backing = try @import("file.zig").Database.openForTesting(std.testing.allocator, tmp.dir, .{        .paths = .{ .database = "database-conflict.db", .wal = "database-conflict.wal" },        .header = testingHeader(),    });    defer backing.deinit();    try backing.reserve(.{ .wal_frames = 1024 });    var catalog = try catalog_mod.Catalog.open(&backing, .{});    _ = try catalog.createRelation(std.testing.allocator, .{        .name = "base",        .columns = &.{.{ .name = "value" }},    }, .{ .durability = .buffered });    _ = try catalog.createRelation(std.testing.allocator, .{        .name = "ours",        .columns = &.{.{ .name = "value" }},    }, .{ .durability = .buffered });    _ = try catalog.createRelation(std.testing.allocator, .{        .name = "theirs",        .columns = &.{.{ .name = "value" }},    }, .{ .durability = .buffered });    var base = try catalog.openRelation(std.testing.allocator, "base");    defer base.deinit();    var ours = try catalog.openRelation(std.testing.allocator, "ours");    defer ours.deinit();    var theirs = try catalog.openRelation(std.testing.allocator, "theirs");    defer theirs.deinit();    _ = try base.relation.put(std.testing.allocator, 1, &.{.{ .text = "base" }}, .{ .durability = .buffered });    _ = try ours.relation.put(std.testing.allocator, 1, &.{.{ .text = "ours" }}, .{ .durability = .buffered });    _ = try theirs.relation.put(std.testing.allocator, 1, &.{.{ .text = "theirs" }}, .{ .durability = .buffered });    const schema = try catalog.schemaState(std.testing.allocator);    var base_root = try version.relationRoot(std.testing.allocator, "items", schema, &base, null);    defer base_root.deinit();    var theirs_root = try version.relationRoot(std.testing.allocator, "items", schema, &theirs, null);    defer theirs_root.deinit();    var ours_value = try version.relationValue(std.testing.allocator, "items", schema, &ours, null);    defer ours_value.deinit(std.testing.allocator);    var ours_database = try databaseValueForRelation(std.testing.allocator, &ours_value);    defer ours_database.deinit();    const carried_artifacts = [_]version.ConflictArtifact{        version.ConflictArtifact.init("items", 1, "old-base", "old-ours", "old-theirs"),        version.ConflictArtifact.init("items", 2, "base-two", "ours-two", "theirs-two"),    };    const carried_entries = [_]version.ConflictEntry{        carried_artifacts[0].entry(),        carried_artifacts[1].entry(),    };    const carried_root = version.ConflictRoot.init(&carried_entries);    const adjusted_ours_root = try version.DatabaseRoot.initSorted(std.testing.allocator, ours_database.root.entries, carried_root);    ours_database.root.deinit();    ours_database.root = adjusted_ours_root;    const carried = ConflictSnapshot{        .root = carried_root,        .artifacts = &carried_artifacts,    };    var result = try mergeDatabase(std.testing.allocator, .{        .relations = &.{.{ .root = &base_root, .rows = .{ .live = &base } }},        .conflicts = carried,    }, .{ .value = &ours_database, .conflicts = carried }, .{        .relations = &.{.{ .root = &theirs_root, .rows = .{ .live = &theirs } }},        .conflicts = carried,    });    defer result.deinit();    try std.testing.expect(result.hasConflicts());    try std.testing.expectEqual(@as(usize, 1), result.discovered.len);    try std.testing.expectEqual(@as(usize, 2), result.conflict_root.count);    try std.testing.expectEqual(@as(usize, 2), result.artifacts.len);    try std.testing.expect(version.same(result.value.root.conflicts, result.conflict_root.hash));    try std.testing.expectEqualStrings("items", result.discovered[0].artifact.relation);    try std.testing.expect(!hasConflictArtifact(result.artifacts, carried_artifacts[0].entry()));    try std.testing.expect(hasConflictArtifact(result.artifacts, carried_artifacts[1].entry()));    try std.testing.expect(hasConflictArtifact(result.artifacts, result.discovered[0].artifact.entry()));    var history = try history_mod.History.open(std.testing.allocator, tmp.dir, .{ .path = "database-conflict.history", .recovery = .reject });    defer history.deinit();    try result.persistConflicts(&history);    const recovered = history.conflict(result.discovered[0].artifact.hash).?;    try std.testing.expectEqualStrings("items", recovered.relation);    const recovered_ours = try row.View.init(recovered.ours.?.row);    try std.testing.expectEqualStrings("ours", (try recovered_ours.column(0)).text);    var root_entries = try history.conflictEntries(std.testing.allocator, result.conflict_root.hash);    defer root_entries.deinit();    try std.testing.expect(version.same(result.conflict_root.hash, root_entries.root.hash));    try std.testing.expectEqual(@as(usize, 2), root_entries.entries.len);    try std.testing.expect(version.same(result.discovered[0].artifact.hash, root_entries.entries[0].hash));    try std.testing.expect(version.same(carried_artifacts[1].hash, root_entries.entries[1].hash));}test "database merge matches relation names and preserves independent relation set changes" {    var tmp = std.testing.tmpDir(.{});    defer tmp.cleanup();    var backing = try @import("file.zig").Database.openForTesting(std.testing.allocator, tmp.dir, .{        .paths = .{ .database = "database-relation-set.db", .wal = "database-relation-set.wal" },        .header = testingHeader(),    });    defer backing.deinit();    try backing.reserve(.{ .wal_frames = 1600 });    var catalog = try catalog_mod.Catalog.open(&backing, .{});    const names = [_][]const u8{        "base_items",        "ours_items",        "theirs_items",        "base_dropped",        "base_drop_on_ours",        "theirs_drop_on_ours",        "base_drop_on_theirs",        "ours_drop_on_theirs",        "ours_only",        "theirs_only",    };    for (names) |name| {        _ = try catalog.createRelation(std.testing.allocator, .{            .name = name,            .columns = &.{.{ .name = "value" }},        }, .{ .durability = .buffered });    }    var base_items = try catalog.openRelation(std.testing.allocator, "base_items");    defer base_items.deinit();    var ours_items = try catalog.openRelation(std.testing.allocator, "ours_items");    defer ours_items.deinit();    var theirs_items = try catalog.openRelation(std.testing.allocator, "theirs_items");    defer theirs_items.deinit();    var base_dropped = try catalog.openRelation(std.testing.allocator, "base_dropped");    defer base_dropped.deinit();    var base_drop_on_ours = try catalog.openRelation(std.testing.allocator, "base_drop_on_ours");    defer base_drop_on_ours.deinit();    var theirs_drop_on_ours = try catalog.openRelation(std.testing.allocator, "theirs_drop_on_ours");    defer theirs_drop_on_ours.deinit();    var base_drop_on_theirs = try catalog.openRelation(std.testing.allocator, "base_drop_on_theirs");    defer base_drop_on_theirs.deinit();    var ours_drop_on_theirs = try catalog.openRelation(std.testing.allocator, "ours_drop_on_theirs");    defer ours_drop_on_theirs.deinit();    var ours_only = try catalog.openRelation(std.testing.allocator, "ours_only");    defer ours_only.deinit();    var theirs_only = try catalog.openRelation(std.testing.allocator, "theirs_only");    defer theirs_only.deinit();    _ = try base_items.relation.put(std.testing.allocator, 1, &.{.{ .text = "same" }}, .{ .durability = .buffered });    _ = try base_items.relation.put(std.testing.allocator, 2, &.{.{ .text = "base" }}, .{ .durability = .buffered });    _ = try ours_items.relation.put(std.testing.allocator, 1, &.{.{ .text = "same" }}, .{ .durability = .buffered });    _ = try ours_items.relation.put(std.testing.allocator, 2, &.{.{ .text = "ours" }}, .{ .durability = .buffered });    _ = try theirs_items.relation.put(std.testing.allocator, 1, &.{.{ .text = "same" }}, .{ .durability = .buffered });    _ = try theirs_items.relation.put(std.testing.allocator, 2, &.{.{ .text = "base" }}, .{ .durability = .buffered });    _ = try theirs_items.relation.put(std.testing.allocator, 3, &.{.{ .text = "theirs" }}, .{ .durability = .buffered });    _ = try base_dropped.relation.put(std.testing.allocator, 9, &.{.{ .text = "dropped" }}, .{ .durability = .buffered });    _ = try base_drop_on_ours.relation.put(std.testing.allocator, 10, &.{.{ .text = "drop-on-ours" }}, .{ .durability = .buffered });    _ = try theirs_drop_on_ours.relation.put(std.testing.allocator, 10, &.{.{ .text = "drop-on-ours" }}, .{ .durability = .buffered });    _ = try base_drop_on_theirs.relation.put(std.testing.allocator, 11, &.{.{ .text = "drop-on-theirs" }}, .{ .durability = .buffered });    _ = try ours_drop_on_theirs.relation.put(std.testing.allocator, 11, &.{.{ .text = "drop-on-theirs" }}, .{ .durability = .buffered });    _ = try ours_only.relation.put(std.testing.allocator, 7, &.{.{ .text = "ours-only" }}, .{ .durability = .buffered });    _ = try theirs_only.relation.put(std.testing.allocator, 8, &.{.{ .text = "theirs-only" }}, .{ .durability = .buffered });    const schema = try catalog.schemaState(std.testing.allocator);    var base_items_root = try version.relationRoot(std.testing.allocator, "items", schema, &base_items, null);    defer base_items_root.deinit();    var base_dropped_root = try version.relationRoot(std.testing.allocator, "dropped", schema, &base_dropped, null);    defer base_dropped_root.deinit();    var base_drop_on_ours_root = try version.relationRoot(std.testing.allocator, "drop_on_ours", schema, &base_drop_on_ours, null);    defer base_drop_on_ours_root.deinit();    var theirs_drop_on_ours_root = try version.relationRoot(std.testing.allocator, "drop_on_ours", schema, &theirs_drop_on_ours, null);    defer theirs_drop_on_ours_root.deinit();    var base_drop_on_theirs_root = try version.relationRoot(std.testing.allocator, "drop_on_theirs", schema, &base_drop_on_theirs, null);    defer base_drop_on_theirs_root.deinit();    var theirs_items_root = try version.relationRoot(std.testing.allocator, "items", schema, &theirs_items, null);    defer theirs_items_root.deinit();    var theirs_only_root = try version.relationRoot(std.testing.allocator, "theirs_only", schema, &theirs_only, null);    defer theirs_only_root.deinit();    var ours_items_value = try version.relationValue(std.testing.allocator, "items", schema, &ours_items, null);    defer ours_items_value.deinit(std.testing.allocator);    var ours_only_value = try version.relationValue(std.testing.allocator, "ours_only", schema, &ours_only, null);    defer ours_only_value.deinit(std.testing.allocator);    var ours_drop_on_theirs_value = try version.relationValue(std.testing.allocator, "drop_on_theirs", schema, &ours_drop_on_theirs, null);    defer ours_drop_on_theirs_value.deinit(std.testing.allocator);    const ours_values = [_]*const version.RelationValue{ &ours_items_value, &ours_drop_on_theirs_value, &ours_only_value };    var ours_database = try databaseValueForRelations(std.testing.allocator, &ours_values);    defer ours_database.deinit();    var result = try mergeDatabase(std.testing.allocator, .{        .relations = &.{            .{ .root = &base_items_root, .rows = .{ .live = &base_items } },            .{ .root = &base_dropped_root, .rows = .{ .live = &base_dropped } },            .{ .root = &base_drop_on_ours_root, .rows = .{ .live = &base_drop_on_ours } },            .{ .root = &base_drop_on_theirs_root, .rows = .{ .live = &base_drop_on_theirs } },        },        .conflicts = .empty(),    }, .{ .value = &ours_database, .conflicts = .empty() }, .{        .relations = &.{            .{ .root = &theirs_only_root, .rows = .{ .live = &theirs_only } },            .{ .root = &theirs_drop_on_ours_root, .rows = .{ .live = &theirs_drop_on_ours } },            .{ .root = &theirs_items_root, .rows = .{ .live = &theirs_items } },        },        .conflicts = .empty(),    });    defer result.deinit();    try std.testing.expect(!result.hasConflicts());    try std.testing.expectEqual(@as(usize, 3), result.value.relations.len);    const items = result.value.findRelation("items").?;    const ours_added = result.value.findRelation("ours_only").?;    const theirs_added = result.value.findRelation("theirs_only").?;    try std.testing.expect(result.value.findRelation("dropped") == null);    try std.testing.expect(result.value.findRelation("drop_on_ours") == null);    try std.testing.expect(result.value.findRelation("drop_on_theirs") == null);    try std.testing.expectEqual(@as(usize, 3), items.rows.len);    const row_two = try row.View.init(relationRowBytes(items.rows, 2).?);    try std.testing.expectEqualStrings("ours", (try row_two.column(0)).text);    const row_three = try row.View.init(relationRowBytes(items.rows, 3).?);    try std.testing.expectEqualStrings("theirs", (try row_three.column(0)).text);    const ours_added_view = try row.View.init(relationRowBytes(ours_added.rows, 7).?);    try std.testing.expectEqualStrings("ours-only", (try ours_added_view.column(0)).text);    const theirs_added_view = try row.View.init(relationRowBytes(theirs_added.rows, 8).?);    try std.testing.expectEqualStrings("theirs-only", (try theirs_added_view.column(0)).text);    var expected = try version.DatabaseRoot.initSorted(std.testing.allocator, &.{        .{ .name = "items", .hash = items.root.hash },        .{ .name = "ours_only", .hash = ours_added.root.hash },        .{ .name = "theirs_only", .hash = theirs_added.root.hash },    }, version.ConflictRoot.empty());    defer expected.deinit();    try std.testing.expect(version.same(expected.hash, result.value.root.hash));}test "database merge records relation topology conflicts under database root" {    var tmp = std.testing.tmpDir(.{});    defer tmp.cleanup();    var backing = try @import("file.zig").Database.openForTesting(std.testing.allocator, tmp.dir, .{        .paths = .{ .database = "database-relation-conflicts.db", .wal = "database-relation-conflicts.wal" },        .header = testingHeader(),    });    defer backing.deinit();    try backing.reserve(.{ .wal_frames = 1600 });    var catalog = try catalog_mod.Catalog.open(&backing, .{});    const names = [_][]const u8{        "base_changed_deleted",        "theirs_changed_deleted",        "base_deleted_changed",        "ours_deleted_changed",        "ours_added",        "theirs_added",    };    for (names) |name| {        _ = try catalog.createRelation(std.testing.allocator, .{            .name = name,            .columns = &.{.{ .name = "value" }},        }, .{ .durability = .buffered });    }    var base_changed_deleted = try catalog.openRelation(std.testing.allocator, "base_changed_deleted");    defer base_changed_deleted.deinit();    var theirs_changed_deleted = try catalog.openRelation(std.testing.allocator, "theirs_changed_deleted");    defer theirs_changed_deleted.deinit();    var base_deleted_changed = try catalog.openRelation(std.testing.allocator, "base_deleted_changed");    defer base_deleted_changed.deinit();    var ours_deleted_changed = try catalog.openRelation(std.testing.allocator, "ours_deleted_changed");    defer ours_deleted_changed.deinit();    var ours_added = try catalog.openRelation(std.testing.allocator, "ours_added");    defer ours_added.deinit();    var theirs_added = try catalog.openRelation(std.testing.allocator, "theirs_added");    defer theirs_added.deinit();    _ = try base_changed_deleted.relation.put(std.testing.allocator, 1, &.{.{ .text = "base-changed-deleted" }}, .{ .durability = .buffered });    _ = try theirs_changed_deleted.relation.put(std.testing.allocator, 1, &.{.{ .text = "theirs-changed-deleted" }}, .{ .durability = .buffered });    _ = try base_deleted_changed.relation.put(std.testing.allocator, 2, &.{.{ .text = "base-deleted-changed" }}, .{ .durability = .buffered });    _ = try ours_deleted_changed.relation.put(std.testing.allocator, 2, &.{.{ .text = "ours-deleted-changed" }}, .{ .durability = .buffered });    _ = try ours_added.relation.put(std.testing.allocator, 3, &.{.{ .text = "ours-added" }}, .{ .durability = .buffered });    _ = try theirs_added.relation.put(std.testing.allocator, 3, &.{.{ .text = "theirs-added" }}, .{ .durability = .buffered });    const schema = try catalog.schemaState(std.testing.allocator);    var base_changed_deleted_root = try version.relationRoot(std.testing.allocator, "changed_deleted", schema, &base_changed_deleted, null);    defer base_changed_deleted_root.deinit();    var theirs_changed_deleted_root = try version.relationRoot(std.testing.allocator, "changed_deleted", schema, &theirs_changed_deleted, null);    defer theirs_changed_deleted_root.deinit();    var base_deleted_changed_root = try version.relationRoot(std.testing.allocator, "deleted_changed", schema, &base_deleted_changed, null);    defer base_deleted_changed_root.deinit();    var theirs_added_root = try version.relationRoot(std.testing.allocator, "added", schema, &theirs_added, null);    defer theirs_added_root.deinit();    var ours_deleted_changed_value = try version.relationValue(std.testing.allocator, "deleted_changed", schema, &ours_deleted_changed, null);    defer ours_deleted_changed_value.deinit(std.testing.allocator);    var ours_added_value = try version.relationValue(std.testing.allocator, "added", schema, &ours_added, null);    defer ours_added_value.deinit(std.testing.allocator);    const ours_values = [_]*const version.RelationValue{ &ours_deleted_changed_value, &ours_added_value };    var ours_database = try databaseValueForRelations(std.testing.allocator, &ours_values);    defer ours_database.deinit();    var result = try mergeDatabase(std.testing.allocator, .{        .relations = &.{            .{ .root = &base_changed_deleted_root, .rows = .{ .live = &base_changed_deleted } },            .{ .root = &base_deleted_changed_root, .rows = .{ .live = &base_deleted_changed } },        },        .conflicts = .empty(),    }, .{ .value = &ours_database, .conflicts = .empty() }, .{        .relations = &.{            .{ .root = &theirs_changed_deleted_root, .rows = .{ .live = &theirs_changed_deleted } },            .{ .root = &theirs_added_root, .rows = .{ .live = &theirs_added } },        },        .conflicts = .empty(),    });    defer result.deinit();    try std.testing.expect(result.hasConflicts());    try std.testing.expectEqual(@as(usize, 0), result.relations.len);    try std.testing.expectEqual(@as(usize, 3), result.discovered.len);    try std.testing.expectEqual(@as(usize, 3), result.conflict_root.count);    try std.testing.expect(version.same(result.value.root.conflicts, result.conflict_root.hash));    try std.testing.expectEqual(@as(usize, 2), result.value.relations.len);    try std.testing.expect(result.value.findRelation("changed_deleted") == null);    const kept_deleted_changed = result.value.findRelation("deleted_changed").?;    const kept_deleted_changed_view = try row.View.init(relationRowBytes(kept_deleted_changed.rows, 2).?);    try std.testing.expectEqualStrings("ours-deleted-changed", (try kept_deleted_changed_view.column(0)).text);    const kept_added = result.value.findRelation("added").?;    const kept_added_view = try row.View.init(relationRowBytes(kept_added.rows, 3).?);    try std.testing.expectEqualStrings("ours-added", (try kept_added_view.column(0)).text);    const added_conflict = findDatabaseConflict(result.discovered, "added").?;    try std.testing.expectEqual(version.ConflictKind.relation, added_conflict.kind);    try std.testing.expect(added_conflict.rowid == null);    try std.testing.expect(added_conflict.base_root == null);    try expectOptionalHash(ours_added_value.root.hash, added_conflict.ours_root);    try expectOptionalHash(theirs_added_root.hash, added_conflict.theirs_root);    try std.testing.expect(added_conflict.artifact.base == null);    try std.testing.expect(version.same(ours_added_value.root.hash, added_conflict.artifact.ours.?.relation));    try std.testing.expect(version.same(theirs_added_root.hash, added_conflict.artifact.theirs.?.relation));    const changed_deleted_conflict = findDatabaseConflict(result.discovered, "changed_deleted").?;    try std.testing.expectEqual(version.ConflictKind.relation, changed_deleted_conflict.kind);    try expectOptionalHash(base_changed_deleted_root.hash, changed_deleted_conflict.base_root);    try std.testing.expect(changed_deleted_conflict.ours_root == null);    try expectOptionalHash(theirs_changed_deleted_root.hash, changed_deleted_conflict.theirs_root);    const deleted_changed_conflict = findDatabaseConflict(result.discovered, "deleted_changed").?;    try std.testing.expectEqual(version.ConflictKind.relation, deleted_changed_conflict.kind);    try expectOptionalHash(base_deleted_changed_root.hash, deleted_changed_conflict.base_root);    try expectOptionalHash(ours_deleted_changed_value.root.hash, deleted_changed_conflict.ours_root);    try std.testing.expect(deleted_changed_conflict.theirs_root == null);    var expected = try version.DatabaseRoot.initSorted(std.testing.allocator, &.{        .{ .name = "added", .hash = kept_added.root.hash },        .{ .name = "deleted_changed", .hash = kept_deleted_changed.root.hash },    }, result.conflict_root);    defer expected.deinit();    try std.testing.expect(version.same(expected.hash, result.value.root.hash));}test "database merge preserves a theirs-only index schema" {    var tmp = std.testing.tmpDir(.{});    defer tmp.cleanup();    var database = try @import("file.zig").Database.openForTesting(std.testing.allocator, tmp.dir, .{        .paths = .{ .database = "schema-select.db", .wal = "schema-select.wal" },        .header = testingHeader(),    });    defer database.deinit();    try database.reserve(.{ .wal_frames = 768 });    var catalog = try catalog_mod.Catalog.open(&database, .{});    _ = try catalog.createRelation(std.testing.allocator, .{        .name = "base",        .columns = &.{.{ .name = "value" }},    }, .{ .durability = .buffered });    _ = try catalog.createRelation(std.testing.allocator, .{        .name = "theirs",        .columns = &.{.{ .name = "value" }},    }, .{ .durability = .buffered });    _ = try catalog.createIndex(std.testing.allocator, "theirs", .{        .name = "theirs_value",        .fields = &.{0},    }, .{ .durability = .buffered });    var base = try catalog.openRelation(std.testing.allocator, "base");    defer base.deinit();    var theirs = try catalog.openRelation(std.testing.allocator, "theirs");    defer theirs.deinit();    const schema = try catalog.schemaState(std.testing.allocator);    var base_value = try version.relationValue(std.testing.allocator, "items", schema, &base, null);    defer base_value.deinit(std.testing.allocator);    var theirs_value = try version.relationValue(std.testing.allocator, "items", schema, &theirs, null);    defer theirs_value.deinit(std.testing.allocator);    var ours_database = try databaseValueForRelation(std.testing.allocator, &base_value);    defer ours_database.deinit();    var result = try mergeDatabase(std.testing.allocator, .{        .relations = &.{.{ .root = &base_value.root, .rows = .{ .materialized = base_value.rows } }},        .conflicts = .empty(),    }, .{ .value = &ours_database, .conflicts = .empty() }, .{        .relations = &.{.{ .root = &theirs_value.root, .rows = .{ .materialized = theirs_value.rows } }},        .conflicts = .empty(),    });    defer result.deinit();    try std.testing.expect(!result.hasConflicts());    try std.testing.expectEqual(RelationMergeMode.theirs, result.relations[0].mode);    try std.testing.expect(version.same(theirs_value.root.hash, result.relations[0].root));    try std.testing.expect(version.same(theirs_value.root.schema, result.value.relations[0].root.schema));    try std.testing.expectEqual(@as(usize, 1), result.value.relations[0].root.indexes.len);}test "database merge contains cross-schema row divergence" {    var tmp = std.testing.tmpDir(.{});    defer tmp.cleanup();    var database = try @import("file.zig").Database.openForTesting(std.testing.allocator, tmp.dir, .{        .paths = .{ .database = "schema-conflict.db", .wal = "schema-conflict.wal" },        .header = testingHeader(),    });    defer database.deinit();    try database.reserve(.{ .wal_frames = 768 });    var catalog = try catalog_mod.Catalog.open(&database, .{});    try createMergeTestRelation(&catalog, "base", false, "base");    try createMergeTestRelation(&catalog, "ours", false, "ours");    try createMergeTestRelation(&catalog, "theirs", true, "theirs");    var base = try catalog.openRelation(std.testing.allocator, "base");    defer base.deinit();    var ours = try catalog.openRelation(std.testing.allocator, "ours");    defer ours.deinit();    var theirs = try catalog.openRelation(std.testing.allocator, "theirs");    defer theirs.deinit();    const schema = try catalog.schemaState(std.testing.allocator);    var base_value = try version.relationValue(std.testing.allocator, "items", schema, &base, null);    defer base_value.deinit(std.testing.allocator);    var ours_value = try version.relationValue(std.testing.allocator, "items", schema, &ours, null);    defer ours_value.deinit(std.testing.allocator);    var theirs_value = try version.relationValue(std.testing.allocator, "items", schema, &theirs, null);    defer theirs_value.deinit(std.testing.allocator);    var ours_database = try databaseValueForRelation(std.testing.allocator, &ours_value);    defer ours_database.deinit();    const base_snapshot = RelationSnapshot{        .root = &base_value.root,        .rows = .{ .materialized = base_value.rows },    };    const theirs_snapshot = RelationSnapshot{        .root = &theirs_value.root,        .rows = .{ .materialized = theirs_value.rows },    };    try std.testing.expectError(error.SchemaMismatch, relation(        std.testing.allocator,        base_snapshot,        .{ .root = &ours_value.root, .rows = .{ .materialized = ours_value.rows } },        theirs_snapshot,    ));    var result = try mergeDatabase(std.testing.allocator, .{        .relations = &.{base_snapshot},        .conflicts = .empty(),    }, .{ .value = &ours_database, .conflicts = .empty() }, .{        .relations = &.{theirs_snapshot},        .conflicts = .empty(),    });    defer result.deinit();    try std.testing.expect(result.hasConflicts());    try std.testing.expectEqual(RelationMergeMode.conflict, result.relations[0].mode);    try std.testing.expect(version.same(ours_value.root.hash, result.relations[0].root));    try std.testing.expectEqual(@as(usize, 0), result.relations[0].result.edits.len);    try std.testing.expectEqual(@as(usize, 1), result.discovered.len);    try std.testing.expectEqual(version.ConflictKind.relation, result.discovered[0].kind);    try std.testing.expect(version.same(base_value.root.hash, result.discovered[0].base_root.?));    try std.testing.expect(version.same(ours_value.root.hash, result.discovered[0].ours_root.?));    try std.testing.expect(version.same(theirs_value.root.hash, result.discovered[0].theirs_root.?));}fn relationRowBytes(rows: []const version.RelationRow, rowid: i64) ?[]const u8 {    for (rows) |row_value| {        if (row_value.rowid == rowid) return row_value.bytes;    }    return null;}fn createMergeTestRelation(    catalog: *const catalog_mod.Catalog,    name: []const u8,    two_columns: bool,    value: []const u8,) !void {    if (two_columns) {        _ = try catalog.createRelation(std.testing.allocator, .{            .name = name,            .columns = &.{ .{ .name = "value" }, .{ .name = "extra" } },        }, .{ .durability = .buffered });    } else {        _ = try catalog.createRelation(std.testing.allocator, .{            .name = name,            .columns = &.{.{ .name = "value" }},        }, .{ .durability = .buffered });    }    var handle = try catalog.openRelation(std.testing.allocator, name);    defer handle.deinit();    if (two_columns) {        _ = try handle.relation.put(std.testing.allocator, 1, &.{            .{ .text = value },            .{ .integer = 7 },        }, .{ .durability = .buffered });    } else {        _ = try handle.relation.put(std.testing.allocator, 1, &.{            .{ .text = value },        }, .{ .durability = .buffered });    }}fn findDatabaseConflict(conflicts: []const DatabaseConflict, relation_name: []const u8) ?*const DatabaseConflict {    for (conflicts) |*conflict| {        if (std.mem.eql(u8, conflict.relation, relation_name)) return conflict;    }    return null;}fn expectOptionalHash(expected: version.Hash, actual: ?version.Hash) !void {    try std.testing.expect(actual != null);    try std.testing.expect(version.same(expected, actual.?));}fn databaseValueForRelation(allocator: Allocator, relation_value: *const version.RelationValue) !version.DatabaseValue {    return try databaseValueForRelations(allocator, &.{relation_value});}fn databaseValueForRelations(allocator: Allocator, relation_values: []const *const version.RelationValue) !version.DatabaseValue {    const relations = try allocator.alloc(version.RelationValue, relation_values.len);    var relation_count: usize = 0;    errdefer {        for (relations[0..relation_count]) |*entry| entry.deinit(allocator);        allocator.free(relations);    }    for (relation_values, relations) |relation_value, *target| {        target.* = try relation_value.clone(allocator);        relation_count += 1;    }    return try version.databaseValueFromOwnedRelations(allocator, relations, version.ConflictRoot.empty().hash);}fn testingHeader() wal.Header {    return .{        .sequence = 2001,        .salt = .{ .first = 0xaaaa_0101, .second = 0xbbbb_0202 },    };}

Source: lib/sql/src/root.zig:38

zig
pub const merge = @import("merge.zig");

Complete call list for merge.mergeDatabase

19 direct calls.

Complete call list for merge.relation

9 direct calls.

Audit

Definitions8
Public names8
Members6
Version26.7.0
Revisiondaab053ee433