Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Merge Algorithm

Horizon Epoch uses a three-way merge algorithm similar to Git, extended for structured data with field-level conflict detection.

Overview

Three-way merge compares:

  1. Base - The common ancestor (merge base)
  2. Ours - The target branch (where we’re merging into)
  3. Theirs - The source branch (what we’re merging)

Visual Overview

                    ┌─────────────────────────────────────────────────────────┐
                    │                  THREE-WAY MERGE                         │
                    └─────────────────────────────────────────────────────────┘

    Source Branch                      Target Branch
    (feature)                          (main)
         │                                  │
         ▼                                  ▼
    ┌─────────┐                        ┌─────────┐
    │Commit E │                        │Commit G │
    │ users:  │                        │ users:  │
    │ Alice→  │                        │ Alice→  │
    │ Smith   │                        │ alice@  │
    └────┬────┘                        │ company │
         │                             └────┬────┘
         │                                  │
         │      ┌──────────────────┐        │
         └──────►    MERGE BASE    ◄────────┘
                │    Commit B      │
                │    users:        │
                │    Alice         │
                │    alice@example │
                └────────┬─────────┘
                         │
                         ▼
                ┌────────────────────┐
                │  MERGE RESULT      │
                │  users:            │
                │  Alice Smith       │  ← from source (name changed)
                │  alice@company.com │  ← from target (email changed)
                └────────────────────┘

Finding the Merge Base

Commit Graph

        C---D---E  (feature)
       /
  A---B---F---G    (main)

The merge base for merging feature into main is commit B - the most recent common ancestor.

Algorithm

def find_merge_base(source_commit, target_commit):
    """Find the lowest common ancestor in the commit DAG."""
    source_ancestors = get_all_ancestors(source_commit)

    # BFS from target to find first intersection
    queue = [target_commit]
    while queue:
        commit = queue.pop(0)
        if commit in source_ancestors:
            return commit
        queue.extend(commit.parents)

    return None  # No common ancestor (shouldn't happen)

Content Hash Optimization

Before performing record-level comparison, Horizon Epoch uses content hashes for quick diff detection:

def should_merge_table(base_version, source_version, target_version):
    """Use content hashes to skip unchanged tables."""
    base_hash = base_version.content_hash
    source_hash = source_version.content_hash
    target_hash = target_version.content_hash

    if source_hash == target_hash:
        # Both branches have identical data - no merge needed
        return MergeAction.SKIP

    if source_hash == base_hash:
        # Only target changed - keep target (fast-forward)
        return MergeAction.KEEP_TARGET

    if target_hash == base_hash:
        # Only source changed - take source
        return MergeAction.TAKE_SOURCE

    # Both changed differently - full record merge required
    return MergeAction.RECORD_LEVEL_MERGE

Three-Way Merge Process

Step 1: Compute Changes

For each table, compute:

  • Changes from base to ours (ours_changes)
  • Changes from base to theirs (theirs_changes)
def compute_changes(base_state, current_state):
    changes = {}

    for record in current_state:
        base_record = base_state.get(record.key)

        if base_record is None:
            changes[record.key] = Change(type=INSERT, record=record)
        elif base_record != record:
            changes[record.key] = Change(type=UPDATE,
                                         base=base_record,
                                         current=record)

    for key in base_state:
        if key not in current_state:
            changes[key] = Change(type=DELETE, base=base_state[key])

    return changes

Step 2: Classify Changes

For each changed record:

OursTheirsResult
No changeChangedAccept theirs
ChangedNo changeKeep ours
Same changeSame changeKeep either (identical)
Different changesDifferent changesCONFLICT
DeletedChangedCONFLICT
ChangedDeletedCONFLICT
DeletedDeletedKeep deleted

Step 3: Apply Non-Conflicting Changes

def apply_changes(base, ours_changes, theirs_changes):
    result = base.copy()
    conflicts = []

    for key in set(ours_changes.keys()) | set(theirs_changes.keys()):
        ours = ours_changes.get(key)
        theirs = theirs_changes.get(key)

        if ours is None:
            # Only theirs changed
            result.apply(theirs)
        elif theirs is None:
            # Only ours changed - already in result
            pass
        elif ours == theirs:
            # Same change - already in result
            pass
        else:
            # Potential conflict - check field level
            conflict = check_field_conflict(key, ours, theirs)
            if conflict:
                conflicts.append(conflict)
            else:
                result.apply(merge_changes(ours, theirs))

    return result, conflicts

Field-Level Conflict Detection

Unlike file-based version control, Horizon Epoch detects conflicts at the field level. This is one of the most powerful features - it allows more changes to merge automatically.

How Field-Level Merge Works

┌─────────────────────────────────────────────────────────────────────────────┐
│                    FIELD-LEVEL MERGE ALGORITHM                               │
└─────────────────────────────────────────────────────────────────────────────┘

For each field in a record where both branches made changes:

┌──────────────────┬──────────────────┬──────────────────┬────────────────────┐
│   Ancestor (A)   │   Source (S)     │   Target (T)     │   Result           │
├──────────────────┼──────────────────┼──────────────────┼────────────────────┤
│   value_a        │   value_a        │   value_a        │   value_a (same)   │
│   value_a        │   value_s        │   value_a        │   value_s (source) │
│   value_a        │   value_a        │   value_t        │   value_t (target) │
│   value_a        │   value_s        │   value_s        │   value_s (same)   │
│   value_a        │   value_s        │   value_t        │   ⚠️ CONFLICT      │
│   NULL           │   value_s        │   NULL           │   value_s (added)  │
│   NULL           │   NULL           │   value_t        │   value_t (added)  │
│   NULL           │   value_s        │   value_t        │   ⚠️ CONFLICT      │
│   value_a        │   NULL           │   value_a        │   NULL (deleted)   │
│   value_a        │   value_a        │   NULL           │   NULL (deleted)   │
│   value_a        │   NULL           │   value_t        │   ⚠️ CONFLICT      │
└──────────────────┴──────────────────┴──────────────────┴────────────────────┘

Implementation

def field_level_merge(ancestor, source, target, strategy):
    """
    Merge two records field-by-field using three-way merge.
    Returns merged record or conflict information.
    """
    result = {}
    conflicting_fields = []

    # Get all fields across all three versions
    all_fields = set(ancestor.keys()) | set(source.keys()) | set(target.keys())

    for field in all_fields:
        ancestor_val = ancestor.get(field)
        source_val = source.get(field)
        target_val = target.get(field)

        # Case 1: Same in both source and target → no conflict
        if source_val == target_val:
            result[field] = source_val
            continue

        # Case 2: Only source changed from ancestor
        if ancestor_val == target_val and ancestor_val != source_val:
            result[field] = source_val
            continue

        # Case 3: Only target changed from ancestor
        if ancestor_val == source_val and ancestor_val != target_val:
            result[field] = target_val
            continue

        # Case 4: Field added in source only
        if ancestor_val is None and target_val is None and source_val is not None:
            result[field] = source_val
            continue

        # Case 5: Field added in target only
        if ancestor_val is None and source_val is None and target_val is not None:
            result[field] = target_val
            continue

        # Case 6: CONFLICT - both changed differently
        conflicting_fields.append(FieldConflict(
            field_name=field,
            ancestor_value=ancestor_val,
            source_value=source_val,
            target_value=target_val
        ))

    if conflicting_fields:
        return MergeRecordResult.Conflict(
            merged_fields=result,
            conflicting_fields=conflicting_fields
        )

    return MergeRecordResult.Resolved(result)

Example: Successful Auto-Merge

Base record:

{"id": 1, "name": "Alice", "email": "alice@example.com", "status": "active"}

Ours (main):

{"id": 1, "name": "Alice", "email": "alice@company.com", "status": "active"}

Theirs (feature):

{"id": 1, "name": "Alice Smith", "email": "alice@example.com", "status": "inactive"}

Field-by-Field Analysis

┌─────────┬───────────────┬───────────────┬───────────────┬────────────────────┐
│ Field   │ Base          │ Ours (main)   │ Theirs (feat) │ Result             │
├─────────┼───────────────┼───────────────┼───────────────┼────────────────────┤
│ id      │ 1             │ 1             │ 1             │ 1 (unchanged)      │
│ name    │ Alice         │ Alice         │ Alice Smith   │ Alice Smith ✓      │
│         │               │ (unchanged)   │ (changed)     │ (take theirs)      │
│ email   │ alice@example │ alice@company │ alice@example │ alice@company ✓    │
│         │               │ (changed)     │ (unchanged)   │ (keep ours)        │
│ status  │ active        │ active        │ inactive      │ inactive ✓         │
│         │               │ (unchanged)   │ (changed)     │ (take theirs)      │
└─────────┴───────────────┴───────────────┴───────────────┴────────────────────┘

Result (auto-merged):

{"id": 1, "name": "Alice Smith", "email": "alice@company.com", "status": "inactive"}

Example: Field-Level Conflict

If both branches changed the same field to different values:

┌─────────┬───────────────┬────────────────────┬────────────────────┬──────────┐
│ Field   │ Base          │ Ours (main)        │ Theirs (feature)   │ Result   │
├─────────┼───────────────┼────────────────────┼────────────────────┼──────────┤
│ email   │ alice@example │ alice@company.com  │ a.smith@example    │ CONFLICT │
│         │               │ (changed)          │ (changed)          │ ⚠️       │
└─────────┴───────────────┴────────────────────┴────────────────────┴──────────┘

Conflict Types

Horizon Epoch distinguishes several conflict types:

#![allow(unused)]
fn main() {
pub enum RecordConflictType {
    /// Both sides modified the same record differently
    ModifyModify,

    /// Source deleted, target modified
    DeleteModify,

    /// Source modified, target deleted
    ModifyDelete,

    /// Both sides added a record with the same primary key
    AddAdd,

    /// Other edge cases
    Other,
}
}

Each type may require different resolution strategies.

Merge Strategies

Horizon Epoch supports four merge strategies, each suited for different workflows:

┌─────────────────────────────────────────────────────────────────────────────┐
│                         MERGE STRATEGIES                                     │
└─────────────────────────────────────────────────────────────────────────────┘

┌─────────────────┬────────────────────────────────────────────────────────────┐
│ Strategy        │ Behavior                                                    │
├─────────────────┼────────────────────────────────────────────────────────────┤
│ ThreeWay        │ Standard 3-way merge, reports conflicts for resolution     │
│ (default)       │ Use when: You want to review and manually resolve conflicts│
├─────────────────┼────────────────────────────────────────────────────────────┤
│ AcceptSource    │ Auto-resolve conflicts by taking source branch values      │
│ (--strategy     │ Use when: Source branch should "win" (e.g., hotfixes)      │
│  theirs)        │                                                            │
├─────────────────┼────────────────────────────────────────────────────────────┤
│ AcceptTarget    │ Auto-resolve conflicts by keeping target branch values     │
│ (--strategy     │ Use when: Target branch should "win" (preserve main)       │
│  ours)          │                                                            │
├─────────────────┼────────────────────────────────────────────────────────────┤
│ FailOnConflict  │ Abort immediately if any conflict is detected              │
│ (--fail-on-     │ Use when: CI/CD pipelines, automated processes             │
│  conflict)      │                                                            │
└─────────────────┴────────────────────────────────────────────────────────────┘

Default Strategy (ThreeWay)

Field-level three-way merge with conflict detection. Non-conflicting changes merge automatically; conflicts are reported for manual resolution.

epoch merge feature/branch

Accept Source (Theirs) Strategy

On conflicts, automatically choose the source branch (the branch being merged in):

epoch merge feature/branch --strategy theirs

Use cases:

  • Hotfix branches that must override any conflicting changes
  • Feature branches developed by domain experts
  • Emergency deployments

Accept Target (Ours) Strategy

On conflicts, automatically keep the target branch values (the branch you’re merging into):

epoch merge feature/branch --strategy ours

Use cases:

  • Integrating experimental branches where existing data should be preserved
  • Rebasing feature branches onto main
  • Pulling updates while preserving local modifications

Fail on Conflict Strategy

Fail immediately if any conflict is detected. Useful for automated pipelines.

epoch merge feature/branch --fail-on-conflict

Use cases:

  • CI/CD pipelines where conflicts indicate process failures
  • Automated data sync jobs
  • Pre-merge validation checks

Strategy Comparison

ScenarioThreeWayAcceptSourceAcceptTargetFailOnConflict
Non-conflicting changes✓ Merge✓ Merge✓ Merge✓ Merge
Same field, different values⚠️ ReportTake sourceKeep target❌ Abort
Delete vs modify⚠️ ReportDeleteKeep modified❌ Abort
Both add same key⚠️ ReportTake sourceKeep target❌ Abort

Conflict Resolution

Interactive Resolution

epoch conflicts resolve --interactive

Walks through each conflict:

Conflict in users.email (record id=1):
  BASE:   alice@example.com
  OURS:   alice@company.com
  THEIRS: alice.smith@example.com

Choose:
  [o] Keep ours
  [t] Keep theirs
  [b] Keep base
  [c] Custom value
>

Programmatic Resolution

conflicts = client.get_conflicts()

for conflict in conflicts:
    for field in conflict.fields:
        # Custom resolution logic
        if field.name == "email":
            # Business rule: prefer company domain
            if "@company.com" in field.ours_value:
                resolution = field.ours_value
            else:
                resolution = field.theirs_value

            client.resolve_conflict(
                table=conflict.table_name,
                record_id=conflict.record_id,
                field=field.name,
                value=resolution
            )

client.merge_continue()

Schema Merge

Schema changes are also merged:

Compatible Changes

These merge automatically:

  • Adding a column (either branch)
  • Adding an index
  • Changing a default value (non-conflicting)

Conflicting Changes

These require resolution:

  • Same column changed to different types
  • Same column renamed differently
  • Conflicting constraints
Schema conflict in users:
  Column 'status':
    OURS:   VARCHAR(20)
    THEIRS: INTEGER

Choose resolution:
  [o] Keep VARCHAR(20)
  [t] Change to INTEGER
  [c] Custom type
>

Merge Commits

After successful merge, a merge commit is created:

Commit: ghi789
Parents: abc123 (main), def456 (feature)
Message: Merge feature/branch into main

Changes:
  users: 5 records merged, 2 conflicts resolved

Performance

Complexity

  • Finding merge base: O(commit history depth)
  • Computing changes: O(changed records)
  • Conflict detection: O(changed records)

Optimization

  1. Change tracking - Only examine records marked as changed
  2. Parallel comparison - Compare multiple tables concurrently
  3. Early termination - Stop at first conflict if not auto-resolving

Best Practices

  1. Merge frequently - Smaller merges have fewer conflicts
  2. Keep branches focused - One feature per branch reduces conflicts
  3. Communicate on shared data - Coordinate changes to same records
  4. Test after merge - Verify data integrity post-merge
  5. Document conflict resolution - Include context in merge commit