Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dev/reasoner/datalog.clj
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
(def ledger @(fluree/create conn "test/rule"))

(def db @(fluree/stage
(fluree/db ledger)
@(fluree/db conn "test/rule")
{"@context" {"ex" "http://example.org/"}
"insert" [{"@id" "ex:brian"
"ex:name" "Brian"
Expand Down
2 changes: 1 addition & 1 deletion dev/user.clj
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
(def ledger @(fluree/create file-conn ledger-alias))

(def db1 @(fluree/stage
(fluree/db ledger)
@(fluree/db file-conn ledger-alias)
{"@context" default-context
"insert" [{:id :ex/brian,
:type :ex/User,
Expand Down
91 changes: 86 additions & 5 deletions src/fluree/db/api.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
(:require [camel-snake-kebab.core :refer [->camelCaseString]]
[clojure.core.async :as async :refer [go <!]]
[clojure.walk :refer [postwalk]]
[fluree.db.api.branch :as api.branch]
[fluree.db.api.transact :as transact-api]
[fluree.db.connection :as connection :refer [connection?]]
[fluree.db.connection.config :as config]
Expand Down Expand Up @@ -266,8 +267,12 @@
(connection/primary-address conn ledger-alias))

(defn load
"Loads an existing ledger by alias or address.
Returns a promise that resolves to the latest database."
"Loads an existing ledger by alias or address and returns its current database.

If alias includes a branch (e.g., 'my-db:feature'), loads that specific branch.
If no branch is specified (e.g., 'my-db'), loads the default 'main' branch.

Returns a promise that resolves to the current database from the loaded branch."
[conn alias-or-address]
(validate-connection conn)
(promise-wrap
Expand Down Expand Up @@ -551,16 +556,92 @@
(let [ledger (<? (connection/load-ledger conn ledger-id))]
(ledger/status ledger)))))

;; Branch operations

(defn create-branch!
"Creates a new branch from an existing branch.

Parameters:
conn - Connection object
new-branch-spec - Full branch spec (e.g., 'ledger:new-branch')
from-branch-spec - Source branch spec (e.g., 'ledger:old-branch')
from-commit - (optional) Specific commit ID to branch from, defaults to latest

Returns promise resolving to the new branch metadata."
([conn new-branch-spec from-branch-spec]
(create-branch! conn new-branch-spec from-branch-spec nil))
([conn new-branch-spec from-branch-spec from-commit]
(validate-connection conn)
(promise-wrap
(api.branch/create-branch! conn new-branch-spec from-branch-spec from-commit))))

(defn list-branches
"Lists all available branches for a ledger.

Parameters:
conn - Connection object
ledger-alias - Ledger alias string (without branch)

Returns promise resolving to a vector of branch names."
[conn ledger-alias]
(validate-connection conn)
(promise-wrap
(api.branch/list-branches conn ledger-alias)))

(defn branch-info
"Returns detailed information about a specific branch.

Parameters:
conn - Connection object
branch-spec - Full branch spec (e.g., \"ledger:branch\")

Returns branch metadata including creation info, head commit, etc."
[conn branch-spec]
(validate-connection conn)
(promise-wrap
(api.branch/branch-info conn branch-spec)))

(defn delete-branch!
"Deletes a branch.

Parameters:
conn - Connection object
branch-spec - Full branch spec to delete (e.g., \"ledger:branch\")

Cannot delete the default branch or protected branches.
Returns promise resolving when deletion is complete."
[conn branch-spec]
(validate-connection conn)
(promise-wrap
(api.branch/delete-branch! conn branch-spec)))

(defn rename-branch!
"Renames a branch.

Parameters:
conn - Connection object
old-branch-spec - Current branch spec (e.g., \"ledger:old-branch\")
new-branch-spec - New branch spec (e.g., \"ledger:new-branch\")

Returns promise resolving when rename is complete."
[conn old-branch-spec new-branch-spec]
(validate-connection conn)
(promise-wrap
(api.branch/rename-branch! conn old-branch-spec new-branch-spec)))

;; db operations

(defn db
"Returns the current database value from a ledger.
"Returns a database value from a ledger.

Loads the specified ledger and returns its current database value.

Parameters:
conn - Connection object
ledger-id - Ledger alias or address
ledger-id - Ledger alias or address (format: 'ledger:branch' or just 'ledger')
If no branch is specified, defaults to ':main'

Returns the current database value."
Returns a promise that resolves to a database value for querying."
[conn ledger-id]
(validate-connection conn)
(promise-wrap
Expand Down
196 changes: 196 additions & 0 deletions src/fluree/db/api/branch.cljc
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
(ns fluree.db.api.branch
"Internal branch operations for Fluree DB.
This namespace contains the implementation logic for branch management."
(:require [fluree.db.connection :as connection]
[fluree.db.constants :as const]
[fluree.db.flake.commit-data :as commit-data]
[fluree.db.ledger :as ledger]
[fluree.db.nameservice :as nameservice]
[fluree.db.util :as util]
[fluree.db.util.async :refer [<? go-try]]
[fluree.db.util.branch :as util.branch]
[fluree.db.util.ledger :as util.ledger]
[fluree.db.util.log :as log]))

(defn create-branch!
"Creates a new branch from an existing branch.

Parameters:
conn - Connection object
new-branch-spec - Full branch spec (e.g., 'ledger:new-branch')
from-branch-spec - Source branch spec (e.g., 'ledger:old-branch')
from-commit - (optional) Specific commit id (sha256 URI) to branch from, defaults to latest

Returns the new branch metadata."
[conn new-branch-spec from-branch-spec from-commit]
(go-try
(let [[ledger-id new-branch] (util.ledger/ledger-parts new-branch-spec)
[from-ledger-id from-branch] (util.ledger/ledger-parts from-branch-spec)]

(when (not= ledger-id from-ledger-id)
(throw (ex-info "Cannot create branch across different ledgers"
{:status 400 :error :db/invalid-branch-operation})))

;; Load source ledger to get its current commit
(let [source-ledger (<? (connection/load-ledger conn from-branch-spec))
source-db (ledger/current-db source-ledger)
;; Prefer commit ID (sha256 URI) for lineage as it's consistent across storage backends
source-commit-id (or from-commit (get-in source-db [:commit :id]))

;; Create branch metadata
metadata {:created-at (util/current-time-iso)
:source-branch from-branch
:source-commit source-commit-id}

;; Prepare commit for new branch with flat metadata fields
source-commit-map (:commit source-db)
compact-commit (-> source-commit-map
commit-data/->json-ld
(assoc "alias" new-branch-spec
"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.

secondary-publishers (:secondary-publishers conn)
_ (log/debug "create-branch! publishing commit for" new-branch-spec
"with primary-publisher?" (boolean primary-publisher))
_ (when primary-publisher
(log/debug "Publishing commit with alias:" (get compact-commit "alias")
"address:" (get compact-commit "address")
"t:" (get-in compact-commit ["data" "t"]))
(<? (nameservice/publish primary-publisher compact-commit))
(log/debug "Published commit for" new-branch-spec)
;; Also publish to secondary publishers asynchronously
(nameservice/publish-to-all compact-commit secondary-publishers))]

(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.

"Check if a nameservice record belongs to a specific ledger.
The ledger field in nameservice records can be either a string or an object with @id."
[ledger-alias record]
(let [ledger-obj (get record "f:ledger")
ledger-name (if (map? ledger-obj)
(get ledger-obj "@id")
ledger-obj)]
(= ledger-name ledger-alias)))

(defn- main-branch?
"Check if a branch name represents the main/default branch.
Returns true for 'main' or nil (which defaults to main)."
[branch-name]
(or (= branch-name const/default-branch-name)
(nil? branch-name)))

(defn list-branches
"Lists all available branches for a ledger.

Parameters:
conn - Connection object
ledger-alias - Ledger alias string (without branch)

Returns a vector of branch names."
[conn ledger-alias]
(go-try
(log/debug "list-branches for ledger:" ledger-alias)
(if-some [primary-publisher (:primary-publisher conn)]
(let [records (<? (nameservice/all-records primary-publisher))
;; Extract branches for the specified ledger
branches (->> records
(filter (partial same-ledger? ledger-alias))
(mapv #(get % "f:branch")))]
(log/debug "Found branches:" branches "for ledger:" ledger-alias)
branches)
(throw (ex-info "No nameservice available for querying branches"
{:status 400 :error :db/no-nameservice})))))

(defn branch-info
"Returns detailed information about a specific branch.

Parameters:
conn - Connection object
branch-spec - Full branch spec (e.g., \"ledger:branch\")

Returns branch metadata including creation info, head commit, etc."
[conn branch-spec]
(go-try
(let [branch-ledger (<? (connection/load-ledger conn branch-spec))]
(<? (ledger/branch-info branch-ledger)))))

(defn delete-branch!
"Deletes a branch.

Parameters:
conn - Connection object
branch-spec - Full branch spec to delete (e.g., \"ledger:branch\")

Cannot delete the default branch or protected branches.
Returns when deletion is complete."
[conn branch-spec]
(go-try
(let [branch-spec* (util.ledger/ensure-ledger-branch branch-spec)
[_ledger-id branch] (util.ledger/ledger-parts branch-spec*)
_ (when (main-branch? branch)
(throw (ex-info "Cannot delete the main branch. Use the drop API to remove the entire ledger."
{:status 400 :error :db/cannot-delete-main-branch})))
ledger (<? (connection/load-ledger conn branch-spec*))
branch-info (<? (ledger/branch-info ledger))]
(when (:protected branch-info)
(throw (ex-info (str "Cannot delete protected branch: " branch)
{:status 400 :error :db/cannot-delete-protected-branch})))
(if-let [primary-publisher (:primary-publisher conn)]
(do
(<? (nameservice/retract primary-publisher branch-spec*))
(connection/release-ledger conn branch-spec*))
(throw (ex-info "No nameservice available for branch deletion"
{:status 400 :error :db/no-nameservice})))
{:deleted branch-spec*})))

(defn rename-branch!
"Renames a branch.

Parameters:
conn - Connection object
old-branch-spec - Current branch spec (e.g., \"ledger:old-branch\")
new-branch-spec - New branch spec (e.g., \"ledger:new-branch\")

Returns when rename is complete."
[conn old-branch-spec new-branch-spec]
(go-try
(let [old-branch-spec* (util.ledger/ensure-ledger-branch old-branch-spec)
new-branch-spec* (util.ledger/ensure-ledger-branch new-branch-spec)
[old-ledger-id old-branch] (util.ledger/ledger-parts old-branch-spec*)
[new-ledger-id new-branch] (util.ledger/ledger-parts new-branch-spec*)]

(when (not= old-ledger-id new-ledger-id)
(throw (ex-info "Cannot rename branch across different ledgers"
{:status 400 :error :db/invalid-branch-operation})))

(when (main-branch? old-branch)
(throw (ex-info "Cannot rename the main branch"
{:status 400 :error :db/cannot-rename-main-branch})))

;; Load the branch to get its current state
(let [ledger (<? (connection/load-ledger conn old-branch-spec*))
branch-info (<? (ledger/branch-info ledger))
_ (when (:protected branch-info)
(throw (ex-info (str "Cannot rename protected branch: " old-branch)
{:status 400 :error :db/cannot-rename-protected-branch})))

source-db (ledger/current-db ledger)
source-commit-map (:commit source-db)

updated-commit (-> source-commit-map
commit-data/->json-ld
(assoc "alias" new-branch-spec*
"branch" new-branch)
(util.branch/augment-commit-with-metadata branch-info))]

(if-let [primary-publisher (:primary-publisher conn)]
(do
(<? (nameservice/publish primary-publisher updated-commit)) ;; Create new branch record
(<? (nameservice/retract primary-publisher old-branch-spec*)) ;; Delete old branch record
{:renamed-from old-branch-spec*
:renamed-to new-branch-spec*})
(throw (ex-info "No nameservice available for branch renaming"
{:status 400 :error :db/no-nameservice})))))))
Loading