aboutsummaryrefslogtreecommitdiffstats
path: root/lib/management
diff options
context:
space:
mode:
authorMarcos Magueta <maguetamarcos@gmail.com>2026-04-03 16:17:38 -0300
committerMarcos Magueta <maguetamarcos@gmail.com>2026-04-03 16:17:38 -0300
commit63ae3c930de89af6f85229a467caa3f53cb844a2 (patch)
treecda4d513672f9fefa09c3a1dff69d8443890e224 /lib/management
parentc5bc3a4992b4968780c286a084ad7c02b2b58f1e (diff)
downloadSakura-63ae3c930de89af6f85229a467caa3f53cb844a2.tar.gz
Sakura-63ae3c930de89af6f85229a467caa3f53cb844a2.zip
format
Diffstat (limited to '')
-rw-r--r--lib/management/branch.ml99
-rw-r--r--lib/management/merge.ml435
-rw-r--r--lib/management/physical.ml135
3 files changed, 354 insertions, 315 deletions
diff --git a/lib/management/branch.ml b/lib/management/branch.ml
index 70dfd85..ea15311 100644
--- a/lib/management/branch.ml
+++ b/lib/management/branch.ml
@@ -4,13 +4,8 @@
- "branch_names" → serialized string list (index for enumeration)
- "head" → branch name as raw bytes *)
-type t = {
- name : string;
- tip : Conventions.Hash.t;
-}
-
+type t = { name : string; tip : Conventions.Hash.t }
type head = string
-
type error = string
let branch_key name = "branch:" ^ name
@@ -18,7 +13,6 @@ let head_key = "head"
let names_key = "branch_names"
module Make (Storage : Physical.S with type error = string) = struct
-
let load_names storage =
match Storage.load_raw storage names_key with
| Ok (Some bytes) -> (Marshal.from_bytes bytes 0 : string list)
@@ -31,13 +25,12 @@ module Make (Storage : Physical.S with type error = string) = struct
let create storage ~name ~tip =
let record = { name; tip } in
let bytes = Marshal.to_bytes record [] in
- (match Storage.store_raw storage (branch_key name) bytes with
- | Error e -> Error e
- | Ok () ->
- let names = load_names storage in
- if not (List.mem name names) then
- store_names storage (name :: names);
- Ok ())
+ match Storage.store_raw storage (branch_key name) bytes with
+ | Error e -> Error e
+ | Ok () ->
+ let names = load_names storage in
+ if not (List.mem name names) then store_names storage (name :: names);
+ Ok ()
let checkout storage branch_name =
let bytes = Bytes.of_string branch_name in
@@ -52,27 +45,30 @@ module Make (Storage : Physical.S with type error = string) = struct
let get_tip storage name =
match Storage.load_raw storage (branch_key name) with
| Ok (Some bytes) ->
- let record : t = Marshal.from_bytes bytes 0 in
- Ok (Some record.tip)
+ let record : t = Marshal.from_bytes bytes 0 in
+ Ok (Some record.tip)
| Ok None -> Ok None
| Error e -> Error e
let update_tip storage ~name ~tip =
match Storage.load_raw storage (branch_key name) with
| Ok (Some _) ->
- let record = { name; tip } in
- let bytes = Marshal.to_bytes record [] in
- Storage.store_raw storage (branch_key name) bytes
+ let record = { name; tip } in
+ let bytes = Marshal.to_bytes record [] in
+ Storage.store_raw storage (branch_key name) bytes
| Ok None -> Error ("Branch not found: " ^ name)
| Error e -> Error e
let list storage =
let names = load_names storage in
- let branches = List.filter_map (fun name ->
- match get_tip storage name with
- | Ok (Some tip) -> Some (name, tip)
- | _ -> None
- ) names in
+ let branches =
+ List.filter_map
+ (fun name ->
+ match get_tip storage name with
+ | Ok (Some tip) -> Some (name, tip)
+ | _ -> None)
+ names
+ in
Ok branches
let branch_relation storage : Relation.t =
@@ -83,23 +79,22 @@ module Make (Storage : Physical.S with type error = string) = struct
let rec generator position =
match position with
| None -> Generator.Error "random access not supported for sakura:branch"
- | Some pos ->
- (match list storage with
- | Error _ -> Generator.Done
- | Ok branches ->
- if pos >= List.length branches then Generator.Done
- else
- let (name, tip) = List.nth branches pos in
- let short = String.sub tip 0 (min 8 (String.length tip)) in
- let attributes =
- Tuple.AttributeMap.of_list
- [ ("name", mk name)
- ; ("hash", mk short)
- ]
- in
- Generator.Value
- ( Tuple.make_materialized ~relation:Prelude.Catalog.branch_rel_name ~attributes
- , generator ))
+ | Some pos -> (
+ match list storage with
+ | Error _ -> Generator.Done
+ | Ok branches ->
+ if pos >= List.length branches then Generator.Done
+ else
+ let name, tip = List.nth branches pos in
+ let short = String.sub tip 0 (min 8 (String.length tip)) in
+ let attributes =
+ Tuple.AttributeMap.of_list
+ [ ("name", mk name); ("hash", mk short) ]
+ in
+ Generator.Value
+ ( Tuple.make_materialized
+ ~relation:Prelude.Catalog.branch_rel_name ~attributes,
+ generator ))
in
Relation.make ~hash:None ~name:Prelude.Catalog.branch_rel_name ~schema
~tree:None ~constraints:None
@@ -115,16 +110,17 @@ module Make (Storage : Physical.S with type error = string) = struct
let generator position =
match position with
| None -> Generator.Error "random access not supported for sakura:head"
- | Some 0 ->
- (match get_head storage with
- | Ok None | Error _ -> Generator.Done
- | Ok (Some branch_name) ->
- let attributes =
- Tuple.AttributeMap.of_list [ ("branch", mk branch_name) ]
- in
- Generator.Value
- ( Tuple.make_materialized ~relation:Prelude.Catalog.head_rel_name ~attributes
- , fun _ -> Generator.Done ))
+ | Some 0 -> (
+ match get_head storage with
+ | Ok None | Error _ -> Generator.Done
+ | Ok (Some branch_name) ->
+ let attributes =
+ Tuple.AttributeMap.of_list [ ("branch", mk branch_name) ]
+ in
+ Generator.Value
+ ( Tuple.make_materialized
+ ~relation:Prelude.Catalog.head_rel_name ~attributes,
+ fun _ -> Generator.Done ))
| Some _ -> Generator.Done
in
Relation.make ~hash:None ~name:Prelude.Catalog.head_rel_name ~schema
@@ -134,5 +130,4 @@ module Make (Storage : Physical.S with type error = string) = struct
~membership_criteria:(fun _ -> true)
~provenance:(Relation.Provenance.Base Prelude.Catalog.head_rel_name)
~lineage:(Relation.Lineage.Base Prelude.Catalog.head_rel_name)
-
end
diff --git a/lib/management/merge.ml b/lib/management/merge.ml
index e50d0e6..d30545a 100644
--- a/lib/management/merge.ml
+++ b/lib/management/merge.ml
@@ -1,40 +1,37 @@
-(** 3-way merge of two branch tips using their Lowest Common Ancestor.
- Walks the history chain to find it, diffs both sides against it,
- applies merge rules, validates constraints, and persists the result. *)
+(** 3-way merge of two branch tips using their Lowest Common Ancestor. Walks the
+ history chain to find it, diffs both sides against it, applies merge rules,
+ validates constraints, and persists the result. *)
type conflict =
- | TupleConflict of { relation: string; hash: Conventions.Hash.t }
- | SchemaConflict of string
+ | TupleConflict of { relation : string; hash : Conventions.Hash.t }
+ | SchemaConflict of string
| ConstraintConflict of string
type strategy = PreferLeft | PreferRight | RevertToAncestor
-
-type merge_result =
- | Clean of Database.t
- | Failed of conflict list
+type merge_result = Clean of Database.t | Failed of conflict list
(** Module type for the subset of Manipulation.Make we need. *)
module type MANIPULATION = sig
type storage
type error
- val of_string_error : string -> error
- val load_database : storage -> Conventions.Hash.t -> (Database.t option, error) result
+
+ val of_string_error : string -> error
+
+ val load_database :
+ storage -> Conventions.Hash.t -> (Database.t option, error) result
+
val store_database : storage -> Database.t -> (unit, error) result
end
-module StringSet = Set.Make(String)
+module StringSet = Set.Make (String)
(** Walk history to find LCA. First hash in right's ancestry that also appears
in left's ancestry set. *)
let find_lca left_db right_db =
let left_ancestors =
- StringSet.of_list (left_db.Database.hash
- :: left_db.Database.history)
- in
- let right_chain =
- right_db.Database.hash
- :: right_db.Database.history
+ StringSet.of_list (left_db.Database.hash :: left_db.Database.history)
in
+ let right_chain = right_db.Database.hash :: right_db.Database.history in
List.find_opt (fun h -> StringSet.mem h left_ancestors) right_chain
(** Apply a single relation diff to a database, using strategy on conflicts. *)
@@ -42,104 +39,140 @@ let apply_diff db ~strategy ~left_db ~right_db ~(left_diff : Diff.relation_diff)
~(right_diff : Diff.relation_diff option) : Database.t * conflict list =
match left_diff with
| Diff.RelationAdded rel ->
- (* Only left added it; right_diff must be None or same *)
- (Database.add_relation db ~relation:rel, [])
-
- | Diff.RelationRemoved name ->
- (Database.remove_relation db ~name, [])
-
- | Diff.RelationModified { name; added_tuples; removed_tuples; schema_changed } ->
- let conflicts = ref [] in
- (* Determine the merged relation's Merkle tree *)
- let base_rel_opt = Database.get_relation db name in
- (match base_rel_opt with
- | None -> (db, [])
- | Some base_rel ->
- let base_tree = Option.value base_rel.Relation.tree ~default:Merkle.empty in
- (* Handle schema conflict *)
- let (resolved_rel, schema_conflict) =
- if schema_changed then
- match right_diff with
- | Some (Diff.RelationModified { schema_changed = true; _ }) ->
- (* Both changed schema — apply strategy *)
- let winner = match strategy with
- | PreferLeft -> Database.get_relation left_db name
- | PreferRight -> Database.get_relation right_db name
- | RevertToAncestor -> Some base_rel
- in
- (Option.value winner ~default:base_rel, [SchemaConflict name])
- | _ ->
- (* Only left changed schema — take left's relation *)
- let left_rel = Option.value
- (Database.get_relation left_db name) ~default:base_rel in
- (left_rel, [])
- else (base_rel, [])
- in
- conflicts := schema_conflict @ !conflicts;
- (* Apply tuple adds/removes from left *)
- let merged_tree =
- List.fold_left (fun t h -> Merkle.insert h t) base_tree added_tuples
- in
- let merged_tree =
- List.fold_left (fun t h -> Merkle.delete h t) merged_tree removed_tuples
- in
- (* Apply tuple adds/removes from right if disjoint *)
- let merged_tree = match right_diff with
- | Some (Diff.RelationModified
+ (* Only left added it; right_diff must be None or same *)
+ (Database.add_relation db ~relation:rel, [])
+ | Diff.RelationRemoved name -> (Database.remove_relation db ~name, [])
+ | Diff.RelationModified { name; added_tuples; removed_tuples; schema_changed }
+ -> (
+ let conflicts = ref [] in
+ (* Determine the merged relation's Merkle tree *)
+ let base_rel_opt = Database.get_relation db name in
+ match base_rel_opt with
+ | None -> (db, [])
+ | Some base_rel ->
+ let base_tree =
+ Option.value base_rel.Relation.tree ~default:Merkle.empty
+ in
+ (* Handle schema conflict *)
+ let resolved_rel, schema_conflict =
+ if schema_changed then
+ match right_diff with
+ | Some (Diff.RelationModified { schema_changed = true; _ }) ->
+ (* Both changed schema — apply strategy *)
+ let winner =
+ match strategy with
+ | PreferLeft -> Database.get_relation left_db name
+ | PreferRight -> Database.get_relation right_db name
+ | RevertToAncestor -> Some base_rel
+ in
+ ( Option.value winner ~default:base_rel,
+ [ SchemaConflict name ] )
+ | _ ->
+ (* Only left changed schema — take left's relation *)
+ let left_rel =
+ Option.value
+ (Database.get_relation left_db name)
+ ~default:base_rel
+ in
+ (left_rel, [])
+ else (base_rel, [])
+ in
+ conflicts := schema_conflict @ !conflicts;
+ (* Apply tuple adds/removes from left *)
+ let merged_tree =
+ List.fold_left (fun t h -> Merkle.insert h t) base_tree added_tuples
+ in
+ let merged_tree =
+ List.fold_left
+ (fun t h -> Merkle.delete h t)
+ merged_tree removed_tuples
+ in
+ (* Apply tuple adds/removes from right if disjoint *)
+ let merged_tree =
+ match right_diff with
+ | Some
+ (Diff.RelationModified
{ added_tuples = r_add; removed_tuples = r_rem; _ }) ->
- (* Detect tuple conflicts: same hash both added and removed *)
- let left_add = StringSet.of_list added_tuples in
- let left_rem = StringSet.of_list removed_tuples in
- let right_add = StringSet.of_list r_add in
- let right_rem = StringSet.of_list r_rem in
- let conflict_hashes =
- StringSet.inter left_add right_rem
- |> StringSet.union (StringSet.inter left_rem right_add)
- in
- if not (StringSet.is_empty conflict_hashes) then begin
- StringSet.iter (fun h ->
- conflicts := TupleConflict { relation = name; hash = h } :: !conflicts
- ) conflict_hashes;
- (* Resolve conflicting tuples per strategy *)
- match strategy with
- | PreferLeft ->
- (* keep left's merged_tree as-is, ignore conflicting right ops *)
- let safe_add = StringSet.diff right_add conflict_hashes in
- let safe_rem = StringSet.diff right_rem conflict_hashes in
- let t = StringSet.fold (fun h acc -> Merkle.insert h acc) safe_add merged_tree in
- StringSet.fold (fun h acc -> Merkle.delete h acc) safe_rem t
- | PreferRight ->
- (* revert left's conflicting ops, apply right's *)
- let t = StringSet.fold (fun h acc -> Merkle.delete h acc) conflict_hashes merged_tree in
- let t = StringSet.fold (fun h acc -> Merkle.insert h acc) right_add t in
- StringSet.fold (fun h acc -> Merkle.delete h acc) right_rem t
- | RevertToAncestor ->
- (* drop conflicting hashes from merged tree *)
- StringSet.fold (fun h acc -> Merkle.delete h acc) conflict_hashes merged_tree
- end else begin
- (* Disjoint — union both sides *)
- let t = List.fold_left (fun acc h -> Merkle.insert h acc) merged_tree r_add in
- List.fold_left (fun acc h -> Merkle.delete h acc) t r_rem
- end
- | _ -> merged_tree
- in
- let new_hash = Hashing.hash_relation
- ~name:resolved_rel.Relation.name
- ~schema:resolved_rel.Relation.schema
- ~tree:merged_tree
- in
- let updated_rel = { resolved_rel with
- Relation.tree = Some merged_tree;
- hash = Some new_hash;
- } in
- let new_db = Database.update_relation db ~relation:updated_rel in
- (new_db, !conflicts))
+ (* Detect tuple conflicts: same hash both added and removed *)
+ let left_add = StringSet.of_list added_tuples in
+ let left_rem = StringSet.of_list removed_tuples in
+ let right_add = StringSet.of_list r_add in
+ let right_rem = StringSet.of_list r_rem in
+ let conflict_hashes =
+ StringSet.inter left_add right_rem
+ |> StringSet.union (StringSet.inter left_rem right_add)
+ in
+ if not (StringSet.is_empty conflict_hashes) then begin
+ StringSet.iter
+ (fun h ->
+ conflicts :=
+ TupleConflict { relation = name; hash = h }
+ :: !conflicts)
+ conflict_hashes;
+ (* Resolve conflicting tuples per strategy *)
+ match strategy with
+ | PreferLeft ->
+ (* keep left's merged_tree as-is, ignore conflicting right ops *)
+ let safe_add = StringSet.diff right_add conflict_hashes in
+ let safe_rem = StringSet.diff right_rem conflict_hashes in
+ let t =
+ StringSet.fold
+ (fun h acc -> Merkle.insert h acc)
+ safe_add merged_tree
+ in
+ StringSet.fold
+ (fun h acc -> Merkle.delete h acc)
+ safe_rem t
+ | PreferRight ->
+ (* revert left's conflicting ops, apply right's *)
+ let t =
+ StringSet.fold
+ (fun h acc -> Merkle.delete h acc)
+ conflict_hashes merged_tree
+ in
+ let t =
+ StringSet.fold
+ (fun h acc -> Merkle.insert h acc)
+ right_add t
+ in
+ StringSet.fold
+ (fun h acc -> Merkle.delete h acc)
+ right_rem t
+ | RevertToAncestor ->
+ (* drop conflicting hashes from merged tree *)
+ StringSet.fold
+ (fun h acc -> Merkle.delete h acc)
+ conflict_hashes merged_tree
+ end
+ else begin
+ (* Disjoint — union both sides *)
+ let t =
+ List.fold_left
+ (fun acc h -> Merkle.insert h acc)
+ merged_tree r_add
+ in
+ List.fold_left (fun acc h -> Merkle.delete h acc) t r_rem
+ end
+ | _ -> merged_tree
+ in
+ let new_hash =
+ Hashing.hash_relation ~name:resolved_rel.Relation.name
+ ~schema:resolved_rel.Relation.schema ~tree:merged_tree
+ in
+ let updated_rel =
+ {
+ resolved_rel with
+ Relation.tree = Some merged_tree;
+ hash = Some new_hash;
+ }
+ in
+ let new_db = Database.update_relation db ~relation:updated_rel in
+ (new_db, !conflicts))
module Make
(Storage : Physical.S)
- (Manip : MANIPULATION with type storage = Storage.t) =
+ (Manip : MANIPULATION with type storage = Storage.t) =
struct
-
let err s = Error (Manip.of_string_error s)
(* Load both tips, find the lowest common ancestor, compute the diffs,
@@ -151,85 +184,103 @@ struct
match Manip.load_database storage left_tip with
| Error e -> Error e
| Ok None -> err ("Left tip not found: " ^ left_tip)
- | Ok (Some left_db) ->
- match Manip.load_database storage right_tip with
- | Error e -> Error e
- | Ok None -> err ("Right tip not found: " ^ right_tip)
- | Ok (Some right_db) ->
- match find_lca left_db right_db with
- | None -> err "No common ancestor found between branches"
- | Some ancestor_hash ->
- match Manip.load_database storage ancestor_hash with
- | Error e -> Error e
- | Ok None -> err ("Ancestor not found: " ^ ancestor_hash)
- | Ok (Some ancestor_db) ->
- let left_diffs = Diff.diff ~ancestor:ancestor_db ~target:left_db in
- let right_diffs = Diff.diff ~ancestor:ancestor_db ~target:right_db in
- let right_diff_map =
- List.fold_left (fun m d ->
- let key = match d with
- | Diff.RelationAdded r -> r.Relation.name
- | Diff.RelationRemoved n -> n
- | Diff.RelationModified { name; _ } -> name
- in
- Database.RelationMap.add key d m
- ) Database.RelationMap.empty right_diffs
- in
- let (merged_db, all_conflicts) =
- let (db_after_left, left_conflicts) =
- List.fold_left (fun (db, conflicts) left_d ->
- let rel_name = match left_d with
- | Diff.RelationAdded r -> r.Relation.name
- | Diff.RelationRemoved n -> n
- | Diff.RelationModified { name; _ } -> name
- in
- let right_d = Database.RelationMap.find_opt rel_name right_diff_map in
- let (new_db, cs) =
- apply_diff db ~strategy ~left_db ~right_db
- ~left_diff:left_d ~right_diff:right_d
- in
- (new_db, cs @ conflicts)
- ) (ancestor_db, []) left_diffs
- in
- let left_names =
- List.fold_left (fun s d ->
- let n = match d with
- | Diff.RelationAdded r -> r.Relation.name
- | Diff.RelationRemoved n -> n
- | Diff.RelationModified { name; _ } -> name
- in
- StringSet.add n s
- ) StringSet.empty left_diffs
- in
- let right_only = List.filter (fun d ->
- let n = match d with
- | Diff.RelationAdded r -> r.Relation.name
- | Diff.RelationRemoved n -> n
- | Diff.RelationModified { name; _ } -> name
- in
- not (StringSet.mem n left_names)
- ) right_diffs in
- let (db_final, right_conflicts) =
- List.fold_left (fun (db, conflicts) right_d ->
- let (new_db, cs) =
- apply_diff db ~strategy ~left_db ~right_db
- ~left_diff:right_d ~right_diff:None
- in
- (new_db, cs @ conflicts)
- ) (db_after_left, left_conflicts) right_only
- in
- (db_final, right_conflicts)
- in
- let unresolvable = List.filter (function
- | ConstraintConflict _ -> true
- | _ -> false
- ) all_conflicts in
- if unresolvable <> [] then
- Ok (Failed unresolvable)
- else begin
- match Manip.store_database storage merged_db with
- | Error e -> Error e
- | Ok () -> Ok (Clean merged_db)
- end
-
+ | Ok (Some left_db) -> (
+ match Manip.load_database storage right_tip with
+ | Error e -> Error e
+ | Ok None -> err ("Right tip not found: " ^ right_tip)
+ | Ok (Some right_db) -> (
+ match find_lca left_db right_db with
+ | None -> err "No common ancestor found between branches"
+ | Some ancestor_hash -> (
+ match Manip.load_database storage ancestor_hash with
+ | Error e -> Error e
+ | Ok None -> err ("Ancestor not found: " ^ ancestor_hash)
+ | Ok (Some ancestor_db) ->
+ let left_diffs =
+ Diff.diff ~ancestor:ancestor_db ~target:left_db
+ in
+ let right_diffs =
+ Diff.diff ~ancestor:ancestor_db ~target:right_db
+ in
+ let right_diff_map =
+ List.fold_left
+ (fun m d ->
+ let key =
+ match d with
+ | Diff.RelationAdded r -> r.Relation.name
+ | Diff.RelationRemoved n -> n
+ | Diff.RelationModified { name; _ } -> name
+ in
+ Database.RelationMap.add key d m)
+ Database.RelationMap.empty right_diffs
+ in
+ let merged_db, all_conflicts =
+ let db_after_left, left_conflicts =
+ List.fold_left
+ (fun (db, conflicts) left_d ->
+ let rel_name =
+ match left_d with
+ | Diff.RelationAdded r -> r.Relation.name
+ | Diff.RelationRemoved n -> n
+ | Diff.RelationModified { name; _ } -> name
+ in
+ let right_d =
+ Database.RelationMap.find_opt rel_name
+ right_diff_map
+ in
+ let new_db, cs =
+ apply_diff db ~strategy ~left_db ~right_db
+ ~left_diff:left_d ~right_diff:right_d
+ in
+ (new_db, cs @ conflicts))
+ (ancestor_db, []) left_diffs
+ in
+ let left_names =
+ List.fold_left
+ (fun s d ->
+ let n =
+ match d with
+ | Diff.RelationAdded r -> r.Relation.name
+ | Diff.RelationRemoved n -> n
+ | Diff.RelationModified { name; _ } -> name
+ in
+ StringSet.add n s)
+ StringSet.empty left_diffs
+ in
+ let right_only =
+ List.filter
+ (fun d ->
+ let n =
+ match d with
+ | Diff.RelationAdded r -> r.Relation.name
+ | Diff.RelationRemoved n -> n
+ | Diff.RelationModified { name; _ } -> name
+ in
+ not (StringSet.mem n left_names))
+ right_diffs
+ in
+ let db_final, right_conflicts =
+ List.fold_left
+ (fun (db, conflicts) right_d ->
+ let new_db, cs =
+ apply_diff db ~strategy ~left_db ~right_db
+ ~left_diff:right_d ~right_diff:None
+ in
+ (new_db, cs @ conflicts))
+ (db_after_left, left_conflicts)
+ right_only
+ in
+ (db_final, right_conflicts)
+ in
+ let unresolvable =
+ List.filter
+ (function ConstraintConflict _ -> true | _ -> false)
+ all_conflicts
+ in
+ if unresolvable <> [] then Ok (Failed unresolvable)
+ else begin
+ match Manip.store_database storage merged_db with
+ | Error e -> Error e
+ | Ok () -> Ok (Clean merged_db)
+ end)))
end
diff --git a/lib/management/physical.ml b/lib/management/physical.ml
index 6063d79..3fc2cf7 100644
--- a/lib/management/physical.ml
+++ b/lib/management/physical.ml
@@ -1,46 +1,48 @@
(** Physical storage abstraction.
- This module defines a functor-based storage interface that can be backed
- by any key-value store (etcd, Redis, DynamoDB, PostgreSQL, filesystem, etc.).
+ This module defines a functor-based storage interface that can be backed by
+ any key-value store (etcd, Redis, DynamoDB, PostgreSQL, filesystem, etc.).
- All data is content-addressed: keys are hashes, values are serialized objects.
- The storage is append-only - objects are never modified, only new versions
- are written with new hashes. *)
+ All data is content-addressed: keys are hashes, values are serialized
+ objects. The storage is append-only - objects are never modified, only new
+ versions are written with new hashes. *)
(** Backend module signature that any storage driver must implement *)
module type BACKEND = sig
type connection
type error
- (** Connect to the storage backend *)
val connect : unit -> (connection, error) result
+ (** Connect to the storage backend *)
- (** Disconnect from the backend *)
val disconnect : connection -> unit
+ (** Disconnect from the backend *)
- (** Get a value by its hash. Returns None if not found. *)
val get : connection -> Conventions.Hash.t -> (bytes option, error) result
+ (** Get a value by its hash. Returns None if not found. *)
- (** Store a value at its hash *)
val put : connection -> Conventions.Hash.t -> bytes -> (unit, error) result
+ (** Store a value at its hash *)
- (** Check if a hash exists *)
val exists : connection -> Conventions.Hash.t -> (bool, error) result
+ (** Check if a hash exists *)
- (** Begin a transaction *)
val begin_transaction : connection -> (unit, error) result
+ (** Begin a transaction *)
- (** Commit a transaction *)
val commit : connection -> (unit, error) result
+ (** Commit a transaction *)
- (** Rollback a transaction *)
val rollback : connection -> (unit, error) result
+ (** Rollback a transaction *)
+ val get_many :
+ connection -> Conventions.Hash.t list -> (bytes option list, error) result
(** Batch get multiple values *)
- val get_many : connection -> Conventions.Hash.t list -> (bytes option list, error) result
+ val put_many :
+ connection -> (Conventions.Hash.t * bytes) list -> (unit, error) result
(** Batch put multiple values *)
- val put_many : connection -> (Conventions.Hash.t * bytes) list -> (unit, error) result
end
(** Storage operations built on top of a backend *)
@@ -48,29 +50,33 @@ module type S = sig
type t
type error
- (** Create a storage instance from a backend connection *)
val create : unit -> (t, error) result
+ (** Create a storage instance from a backend connection *)
- (** Close the storage *)
val close : t -> unit
+ (** Close the storage *)
+ val store_attribute :
+ t -> Conventions.AbstractValue.t -> (Conventions.Hash.t, error) result
(** Store an attribute value, returns its hash *)
- val store_attribute : t -> Conventions.AbstractValue.t -> (Conventions.Hash.t, error) result
+ val load_attribute :
+ t ->
+ Conventions.Hash.t ->
+ (Conventions.AbstractValue.t option, error) result
(** Load an attribute value by hash *)
- val load_attribute : t -> Conventions.Hash.t -> (Conventions.AbstractValue.t option, error) result
- (** Store raw bytes at a hash *)
val store_raw : t -> Conventions.Hash.t -> bytes -> (unit, error) result
+ (** Store raw bytes at a hash *)
- (** Load raw bytes by hash *)
val load_raw : t -> Conventions.Hash.t -> (bytes option, error) result
+ (** Load raw bytes by hash *)
- (** Check if a hash exists *)
val exists : t -> Conventions.Hash.t -> (bool, error) result
+ (** Check if a hash exists *)
- (** Execute a function within a transaction *)
val with_transaction : t -> (unit -> ('a, error) result) -> ('a, error) result
+ (** Execute a function within a transaction *)
end
(** Functor to create storage operations from a backend *)
@@ -79,15 +85,12 @@ module Make (B : BACKEND) : S with type error = B.error = struct
type error = B.error
let create () = B.connect ()
-
let close = B.disconnect
let store_attribute conn value =
let bytes = Marshal.to_bytes value [] in
let hash = Conventions.AbstractValue.hash value in
- match B.put conn hash bytes with
- | Ok () -> Ok hash
- | Error e -> Error e
+ match B.put conn hash bytes with Ok () -> Ok hash | Error e -> Error e
let load_attribute conn hash =
match B.get conn hash with
@@ -95,27 +98,20 @@ module Make (B : BACKEND) : S with type error = B.error = struct
| Ok None -> Ok None
| Error e -> Error e
- let store_raw conn hash bytes =
- B.put conn hash bytes
-
- let load_raw conn hash =
- B.get conn hash
-
- let exists conn hash =
- B.exists conn hash
+ let store_raw conn hash bytes = B.put conn hash bytes
+ let load_raw conn hash = B.get conn hash
+ let exists conn hash = B.exists conn hash
let with_transaction conn f =
match B.begin_transaction conn with
| Error e -> Error e
- | Ok () ->
- match f () with
- | Ok result ->
- (match B.commit conn with
- | Ok () -> Ok result
- | Error e -> Error e)
- | Error e ->
- let _ = B.rollback conn in
- Error e
+ | Ok () -> (
+ match f () with
+ | Ok result -> (
+ match B.commit conn with Ok () -> Ok result | Error e -> Error e)
+ | Error e ->
+ let _ = B.rollback conn in
+ Error e)
end
(** In-memory backend for testing and development *)
@@ -125,35 +121,34 @@ module MemoryBackend : BACKEND with type error = string = struct
mutable in_transaction : bool;
mutable transaction_buffer : (string * bytes) list;
}
+
type error = string
let connect () =
- Ok {
- data = Hashtbl.create 1024;
- in_transaction = false;
- transaction_buffer = [];
- }
+ Ok
+ {
+ data = Hashtbl.create 1024;
+ in_transaction = false;
+ transaction_buffer = [];
+ }
let disconnect _ = ()
-
- let get conn hash =
- Ok (Hashtbl.find_opt conn.data hash)
+ let get conn hash = Ok (Hashtbl.find_opt conn.data hash)
let put conn hash value =
if conn.in_transaction then begin
conn.transaction_buffer <- (hash, value) :: conn.transaction_buffer;
Ok ()
- end else begin
+ end
+ else begin
Hashtbl.replace conn.data hash value;
Ok ()
end
- let exists conn hash =
- Ok (Hashtbl.mem conn.data hash)
+ let exists conn hash = Ok (Hashtbl.mem conn.data hash)
let begin_transaction conn =
- if conn.in_transaction then
- Error "Already in transaction"
+ if conn.in_transaction then Error "Already in transaction"
else begin
conn.in_transaction <- true;
conn.transaction_buffer <- [];
@@ -161,20 +156,18 @@ module MemoryBackend : BACKEND with type error = string = struct
end
let commit conn =
- if not conn.in_transaction then
- Error "Not in transaction"
+ if not conn.in_transaction then Error "Not in transaction"
else begin
- List.iter (fun (hash, value) ->
- Hashtbl.replace conn.data hash value
- ) conn.transaction_buffer;
+ List.iter
+ (fun (hash, value) -> Hashtbl.replace conn.data hash value)
+ conn.transaction_buffer;
conn.in_transaction <- false;
conn.transaction_buffer <- [];
Ok ()
end
let rollback conn =
- if not conn.in_transaction then
- Error "Not in transaction"
+ if not conn.in_transaction then Error "Not in transaction"
else begin
conn.in_transaction <- false;
conn.transaction_buffer <- [];
@@ -185,14 +178,14 @@ module MemoryBackend : BACKEND with type error = string = struct
Ok (List.map (fun h -> Hashtbl.find_opt conn.data h) hashes)
let put_many conn pairs =
- List.iter (fun (hash, value) ->
- if conn.in_transaction then
- conn.transaction_buffer <- (hash, value) :: conn.transaction_buffer
- else
- Hashtbl.replace conn.data hash value
- ) pairs;
+ List.iter
+ (fun (hash, value) ->
+ if conn.in_transaction then
+ conn.transaction_buffer <- (hash, value) :: conn.transaction_buffer
+ else Hashtbl.replace conn.data hash value)
+ pairs;
Ok ()
end
+module Memory = Make (MemoryBackend)
(** Default in-memory storage for development *)
-module Memory = Make(MemoryBackend)