Skip to content

Conversation

@bplatz
Copy link
Contributor

@bplatz bplatz commented Aug 21, 2025

Summary

This PR introduces foundational branching capabilities to Fluree, enabling parallel database development through branch isolation. This establishes the infrastructure for Git-like version control workflows, allowing multiple branches to coexist and evolve independently.

This PR sits on top of #1088 (branch and time travel naming)
This is a prerequisite to #1095 (merge, rebase, reset branch operations)
This is also a prerequisite to #1102 (cuckoo filter for safe index cleanup across branches)

Core Features

1. Branch-Aware API

All Fluree APIs now support branch notation using the : separator:

;; Load specific branches
@(fluree/load conn "mydb:main")
@(fluree/load conn "mydb:feature")

;; Write to specific branches
@(fluree/insert! conn "mydb:feature" {...})
@(fluree/update! conn "mydb:main" {...})

;; Query from branch-specific databases
(def feature-db @(fluree/load conn "mydb:feature"))
@(fluree/query feature-db {...})

2. Branch Creation

Create new branches from existing branches:

;; Create a feature branch from main
@(fluree/create-branch! conn "mydb:feature" "mydb:main")

;; Create another branch from feature
@(fluree/create-branch! conn "mydb:feature-2" "mydb:feature")

3. Branch Isolation

Each branch maintains its own:

  • Commit history
  • Current state
  • Independent evolution

Changes to one branch don't affect others until explicitly merged (merge operations coming in future PRs).

4. Time Travel with SHA Support

Navigate to specific commits using SHA:

;; Time travel using SHA in branch URIs
@(fluree/load conn "mydb:branch@sha256:abc123...")

Implementation Details

Architecture Changes

  • Branch notation: Standardized on : separator (e.g., ledger:branch)
  • Default branch: All ledgers default to main branch
  • Nameservice: Branch metadata stored in dedicated directory structure
  • Ledger validation: Ledger names cannot contain : to avoid conflicts with branch notation

Key Components Modified

  • Connection and ledger loading made branch-aware
  • API functions updated to parse and handle branch specifications
  • Nameservice updated to store branch metadata separately
  • File and memory storage adapted for branch support

Testing

Tests added for:

  • Branch creation and isolation
  • Branch-aware API operations
  • SHA-based time travel
  • File and memory storage compatibility

Run tests with:

make cljtest

Breaking Changes

  • Ledger naming: Ledger names can no longer contain : character (reserved for branch notation)
  • Branch specification: APIs now expect ledger:branch format where branch is specified

Migration

  • Existing ledgers work unchanged - they default to the main branch
  • No data migration required
  • Existing code continues to work by implicitly using the main branch

What This Enables (Future Work)

This foundational work enables future PRs to add:

  • Branch merge operations (rebase, squash, fast-forward)
  • Branch divergence analysis
  • Reset and rollback operations
  • Conflict detection and resolution
  • Cherry-pick functionality
  • Branch protection rules

Examples

Creating a Feature Branch Workflow

;; Start with main branch
@(fluree/create conn "myapp" {})
@(fluree/insert! conn "myapp:main" 
  {"@context" {"ex" "http://example.org/"}
   "@id" "ex:product1"
   "ex:name" "Widget"
   "ex:price" 100})

;; Create feature branch for price update
@(fluree/create-branch! conn "myapp:update-prices" "myapp:main")

;; Work on feature branch
@(fluree/update! conn "myapp:update-prices"
  {"@context" {"ex" "http://example.org/"}
   "where" {"@id" "ex:product1"}
   "delete" {"ex:price" 100}
   "insert" {"ex:price" 110}})

;; Main branch remains unchanged
(def main-db @(fluree/load conn "myapp:main"))
(def main-price @(fluree/query main-db 
  {"select" "?price" 
   "where" {"@id" "ex:product1" "ex:price" "?price"}}))
;; Returns: 100

;; Feature branch has the update
(def feature-db @(fluree/load conn "myapp:update-prices"))
(def feature-price @(fluree/query feature-db
  {"select" "?price"
   "where" {"@id" "ex:product1" "ex:price" "?price"}}))
;; Returns: 110

Next PR

The next PR will add merge operations (rebase, reset) to integrate changes between branches, building on this foundational branching support.

@bplatz bplatz force-pushed the feature/branching branch from 970f0e4 to ce3b475 Compare August 21, 2025 20:12
@bplatz bplatz changed the base branch from main to feature/time-travel-uri August 21, 2025 20:14
@bplatz bplatz force-pushed the feature/time-travel-uri branch from 9738a09 to d7891e7 Compare August 27, 2025 01:56
@bplatz bplatz force-pushed the feature/branching branch from ce3b475 to b2a5f9f Compare August 27, 2025 02:57
@bplatz bplatz marked this pull request as ready for review August 27, 2025 14:45
@bplatz bplatz requested a review from a team August 27, 2025 14:46
@bplatz bplatz force-pushed the feature/time-travel-uri branch from d7891e7 to 42af763 Compare September 6, 2025 15:14
Base automatically changed from feature/time-travel-uri to main September 16, 2025 13:10
@bplatz
Copy link
Contributor Author

bplatz commented Oct 21, 2025

Any more feedback on this? It's been stale for some time? If ready to go I'll update with main and then merge @zonotope @dpetran

(recur r (conj addrs (<? (nameservice/publishing-address nsv ledger-alias))))
(recur r addrs))
(let [published? (<? (nameservice/published-ledger? nsv ledger-alias))]
(log/info "published-addresses: checking" ledger-alias "published?" published?)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this should log at the info level.

(if (<? (nameservice/published-ledger? nsv ledger-alias))
(recur r (conj addrs (<? (nameservice/publishing-address nsv ledger-alias))))
(recur r addrs))
(let [published? (<? (nameservice/published-ledger? nsv ledger-alias))]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the nameservice/published-ledger? call would go better in the if condition directly on line 279.

[ns-record commit-address commit-t]
"Updates commit address if new t is greater than existing t.
For branch creation, also validates that branch doesn't already exist."
[ns-record ledger-alias commit-address commit-t is-branch-creation]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be split into two different functions instead of including the is-branch-creation boolean option here.

;; Extract metadata from incoming data
new-metadata (util.branch/extract-branch-metadata data)
;; Check if this is a branch creation (has source metadata)
is-branch-creation (and (:source-branch new-metadata) (:source-commit new-metadata))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is clearer as a separate branch-creation? function

"branch" new-branch)
(util.branch/augment-commit-with-metadata metadata))

primary-publisher (:primary-publisher conn)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not for this pr as I think we need an explicit way to connect to a nameservice and provide an address for it, but if the primary publisher (or any other publishers for that matter) should be optional, then it shouldn't be tied to the connection. We should be able to use the same connection to create multiple branches, some of which do have publishers configured, and some without.


(util.branch/branch-creation-response new-branch metadata source-commit-id)))))

(defn- same-ledger?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This terminology is confusing to me. I had been using ledger-alias as the full name including the branch, and ledger-name as the name without the branch, and ledger as the catch all for all of a ledgers branches treated as a single unit.

The name of this function and the intermediate bindings used within it do not follow that model. I'm fine with changing my internal terminology, but I do think we should pick consistent terms for those concepts and stick to them.

@bplatz bplatz marked this pull request as draft January 9, 2026 03:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants