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

Cookbook: Branching and Merging

Fluree lets you fork a ledger into independent branches, each with its own commit history. Experiment freely, then merge changes back when ready. Think of it like git branch for your data.

Quick start

# Create a branch from main
fluree branch create experiment

# Switch to the branch
fluree use mydb:experiment

# Make changes (only on the branch)
fluree insert '...'
fluree update '...'

# See both branches
fluree branch list

# Merge back into main
fluree branch merge experiment

# Clean up
fluree branch drop experiment

Core concepts

  • Branches are isolated — Transactions on one branch are invisible to others
  • Branches are cheap — Creating a branch doesn’t copy data; it creates a new commit pointer
  • Merge is fast-forward — The target branch must not have diverged. If it has, rebase first
  • Source branch survives merge — After merging, the branch can continue receiving transactions

Patterns

Safe experimentation

Try a risky change without affecting production:

fluree branch create try-new-schema

fluree use mydb:try-new-schema

# Restructure data
fluree update 'PREFIX ex: <http://example.org/>
DELETE { ?doc ex:category ?cat }
INSERT { ?doc ex:tags ?cat }
WHERE  { ?doc ex:category ?cat }'

# Verify the change looks right
fluree query 'SELECT ?doc ?tag WHERE { ?doc ex:tags ?tag }'

# If it works, merge back
fluree branch merge try-new-schema
fluree branch drop try-new-schema

# If it doesn't work, just drop the branch — main is untouched
fluree branch drop try-new-schema

Review before merge

Use branches as a staging area for data changes:

# Data engineer creates a branch for the weekly import
fluree branch create weekly-import

fluree use mydb:weekly-import

# Import new data
fluree insert -f new-data.ttl

# Verify: count new entities
fluree query 'SELECT (COUNT(?s) AS ?count) WHERE { ?s a ex:NewRecord }'

# Verify: no duplicates
fluree query 'SELECT ?id (COUNT(?s) AS ?count) WHERE {
  ?s ex:externalId ?id
} GROUP BY ?id HAVING(?count > 1)'

# Looks good — merge into main
fluree branch merge weekly-import

Multi-environment workflow

Use branches to model dev/staging/prod environments within a single ledger:

# Create environment branches
fluree branch create staging
fluree branch create dev --from staging

# Developers work on dev
fluree use mydb:dev
fluree insert '...'

# Promote to staging via merge
fluree branch merge dev --target staging

# Promote to main (production) after testing
fluree use mydb:staging
# ... run validation queries ...
fluree branch merge staging

Feature branches

Multiple people can work on different features simultaneously:

# Team member A: add product categories
fluree branch create feature-categories

# Team member B: update pricing
fluree branch create feature-pricing

# Each works independently on their branch
# ...

# Merge sequentially — first one is a fast-forward
fluree branch merge feature-categories

# Second one may need rebase if main advanced
fluree branch rebase feature-pricing
fluree branch merge feature-pricing

Rebase to catch up with upstream

When main has advanced since you branched:

# Main has new commits that your branch doesn't have
fluree branch rebase my-branch

This replays your branch’s commits on top of main’s current HEAD. Conflict strategies:

StrategyBehavior
take-both (default)Keep both the source and branch changes
abortStop if any conflicts — let you inspect
take-sourceSource (main) wins on conflict
take-branchBranch wins on conflict
skipSkip conflicting commits entirely
# Rebase with abort on conflict for manual review
fluree branch rebase my-branch --strategy abort

# Rebase where main always wins
fluree branch rebase my-branch --strategy take-source

Fast-forward indexing and transaction ordering

A fast-forward advances the target to the source’s commit head while retaining the target’s own index. It does not copy or adopt the source’s index, because that index’s reserved graph names belong to the source branch. Queries replay commits after the target’s indexed point until indexing catches up; a target with no index replays from genesis. For a large merged range, run fluree index mydb:main to catch up explicitly.

For embedded applications calling Fluree::merge_branch directly with a Raft nameservice, the fast-forward ref update goes through consensus but bypasses the per-branch work queue. It can overtake transactions already submitted to that queue. The CAS checks the target head observed during merge preparation; if another writer advances it first, the merge returns BranchConflict (409) and should be retried so it recomputes against the new head. Repeating an already-completed fast-forward succeeds without copying commits again.

The server’s Raft merge path uses QueuedTransactor::merge, which queues the merge alongside transactions and prepares it when the worker reaches it. That path preserves queue ordering and was not affected by the direct API’s empty-queue publish failure.

Repair a target that adopted another branch’s index

Older direct fast-forward merges from an indexed source could adopt the source’s index root, giving the target the source branch’s #txn-meta and #config graph labels. Ordinary data queries could still work while the target stopped resolving its own governance configuration. Rewriting the config alone does not repair the graph registry.

Run a full rebuild of the affected target:

fluree reindex mydb:main

In an embedded application, call fluree.reindex("mydb:main", ReindexOptions::default()).await? on the serving instance. After publishing the rebuilt root, reindex evicts that instance’s cached ledger so subsequent loads read the corrected registry, even when the index transaction time is unchanged. Existing handles held by callers must be reacquired; other serving instances must also reload their cached ledger. Older versions without this eviction require a cache disconnect or process restart after reindex.

An affected root can also cause graph_iris[0] must be txn-meta IRI on the second incremental indexing cycle: the first cycle stamps the target’s ledger ID onto the root but retains the source’s graph labels. The normal indexer falls back to a full rebuild when this validation fails; it does not permanently stop indexing. Explicit reindex repairs the target without waiting for those cycles.

Compare branches

See what’s different between two branches:

# Query branch for entities not in main
fluree query --ledger mydb:my-branch 'SELECT ?s ?p ?o WHERE {
  ?s ?p ?o .
  FILTER NOT EXISTS {
    SERVICE <fluree:ledger:mydb:main> { ?s ?p ?o }
  }
}'

Time travel across branches

Each branch has its own transaction history. Query any branch at any point in time:

# Branch state after its 3rd transaction
fluree query --ledger mydb:experiment --at 3 'SELECT ?s ?p ?o WHERE { ?s ?p ?o }'

Branch at a historical point

By default, branch create starts the new branch at the source’s current HEAD. Pass --at to start it at an earlier commit on the source branch instead — useful for recovering to a known-good state, forking off an older release, or experimenting with what-if scenarios from a past point in time.

# Start a branch at transaction 5 on main
fluree branch create rewind --at t:5

# Or use a hex-digest prefix of the commit
fluree branch create rewind --at 3dd028a7

The commit must be reachable from the source branch’s HEAD (branching from an unrelated branch’s commit is rejected). The new branch starts with no index and replays from genesis on first query — acceptable for small/medium histories; if replay cost matters, transact a small no-op to force an index rebuild.

Full CIDs are also accepted (--at fluree:commit:sha256:...) and resolve without requiring the source to be indexed; t:N and hex prefixes require an indexed source.

Branch lifecycle

create ──→ transact ──→ rebase (if needed) ──→ merge ──→ drop
              ↑                                  │
              └──────── continue working ←───────┘

After merging, the branch is still alive. You can:

  • Continue transacting on it (for ongoing work)
  • Merge again later (only new commits since last merge are copied)
  • Drop it when done

HTTP API

# Create a branch
curl -X POST http://localhost:8090/v1/fluree/branch \
  -H "Content-Type: application/json" \
  -d '{"ledger": "mydb", "branch": "dev", "source": "main"}'

# Branch at a historical commit
curl -X POST http://localhost:8090/v1/fluree/branch \
  -H "Content-Type: application/json" \
  -d '{"ledger": "mydb", "branch": "rewind", "at": "t:5"}'

# Query a specific branch
curl -X POST 'http://localhost:8090/v1/fluree/query?ledger=mydb:dev' \
  -H "Content-Type: application/sparql-query" \
  -d 'SELECT ?s ?p ?o WHERE { ?s ?p ?o }'

# Merge
curl -X POST http://localhost:8090/v1/fluree/merge \
  -H "Content-Type: application/json" \
  -d '{"ledger": "mydb", "source": "dev"}'

Best practices

  1. Name branches descriptivelyweekly-import-2025-04, feature-product-tags, not test1
  2. Keep branches short-lived — Long-lived branches diverge more, making rebase harder
  3. Merge frequently — Small, frequent merges are easier than large, infrequent ones
  4. Test before merging — Run validation queries on the branch before promoting
  5. Drop after merging — Clean up branches you’re done with