tiny.sql.merge
Defined in tiny.sql.
API (16)
Actions
Public operations.
Types and contracts
Public types and contracts.
ConflictConflictSnapshotDatabaseConflictDatabaseMergeDatabaseRelationMergeDatabaseSnapshotDatabaseSourceEditEditKindErrorRelationMergeRelationMergeModeRelationSnapshot
Source
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.
lib.sql.src.merge.DatabaseRelationMerge.deinit[method] — private source atlib/sql/src/merge.zig:127in nearest public ownertiny.sql.mergelib.sql.src.merge.appendClonedRelationValue[function] — private source atlib/sql/src/merge.zig:681in nearest public ownertiny.sql.mergelib.sql.src.merge.appendDatabaseConflict[function] — private source atlib/sql/src/merge.zig:763in nearest public ownertiny.sql.mergelib.sql.src.merge.appendRelationConflict[function] — private source atlib/sql/src/merge.zig:776in nearest public ownertiny.sql.mergelib.sql.src.merge.appendSnapshotNames[function] — private source atlib/sql/src/merge.zig:565in nearest public ownertiny.sql.mergelib.sql.src.merge.appendSnapshotRelationValue[function] — private source atlib/sql/src/merge.zig:689in nearest public ownertiny.sql.mergelib.sql.src.merge.appendValueNames[function] — private source atlib/sql/src/merge.zig:569in nearest public ownertiny.sql.mergelib.sql.src.merge.databaseConflict[function] — private source atlib/sql/src/merge.zig:543in nearest public ownertiny.sql.mergelib.sql.src.merge.deinitConflictArtifact[function] — private source atlib/sql/src/merge.zig:513in nearest public ownertiny.sql.mergelib.sql.src.merge.findConflictArtifact[function] — private source atlib/sql/src/merge.zig:477in nearest public ownertiny.sql.mergelib.sql.src.merge.findSnapshot[function] — private source atlib/sql/src/merge.zig:584in nearest public ownertiny.sql.mergelib.sql.src.merge.mergeExistingRelation[function] — private source atlib/sql/src/merge.zig:597in nearest public ownertiny.sql.mergelib.sql.src.merge.reconcileConflictArtifacts[function] — private source atlib/sql/src/merge.zig:416in nearest public ownertiny.sql.mergelib.sql.src.merge.validateConflictSnapshot[function] — private source atlib/sql/src/merge.zig:377in nearest public ownertiny.sql.mergetiny.sql.ConflictRoot.empty[function] atlib/sql/src/version.zig:225tiny.sql.ConflictRoot.init[function] atlib/sql/src/version.zig:231tiny.sql.RelationValue.deinit[method] atlib/sql/src/version.zig:166tiny.sql.version.databaseValueFromOwnedRelations[function] atlib/sql/src/version.zig:949tiny.sql.version.same[function] atlib/sql/src/version.zig:498
Complete call list for merge.relation
9 direct calls.
lib.sql.src.merge.Conflict.deinit[method] — private source atlib/sql/src/merge.zig:73in nearest public ownertiny.sql.mergelib.sql.src.merge.Edit.deinit[method] — private source atlib/sql/src/merge.zig:58in nearest public ownertiny.sql.mergelib.sql.src.merge.appendConflict[function] — private source atlib/sql/src/merge.zig:742in nearest public ownertiny.sql.mergelib.sql.src.merge.appendEdit[function] — private source atlib/sql/src/merge.zig:726in nearest public ownertiny.sql.mergelib.sql.src.merge.entryBytes[function] — private source atlib/sql/src/merge.zig:833in nearest public ownertiny.sql.mergelib.sql.src.merge.entryMatches[function] — private source atlib/sql/src/merge.zig:840in nearest public ownertiny.sql.mergelib.sql.src.merge.lowestRowid[function] — private source atlib/sql/src/merge.zig:844in nearest public ownertiny.sql.mergelib.sql.src.merge.sameBytes[function] — private source atlib/sql/src/merge.zig:827in nearest public ownertiny.sql.mergelib.sql.src.merge.schemasAgree[function] — private source atlib/sql/src/merge.zig:640in nearest public ownertiny.sql.merge
Audit
| Definitions | 8 |
|---|---|
| Public names | 8 |
| Members | 6 |
| Version | 26.7.0 |
| Revision | daab053ee433 |