aboutsummaryrefslogtreecommitdiffstats
path: root/lib/management
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--lib/management/branch.ml8
-rw-r--r--lib/management/database.ml136
-rw-r--r--lib/management/diff.ml70
-rw-r--r--lib/management/merge.ml288
-rw-r--r--lib/management/multigroup.ml84
5 files changed, 88 insertions, 498 deletions
diff --git a/lib/management/branch.ml b/lib/management/branch.ml
index 81da225..801c43d 100644
--- a/lib/management/branch.ml
+++ b/lib/management/branch.ml
@@ -70,7 +70,7 @@ module Make (Storage : Physical.S with type error = string) = struct
names
in
Ok branches
-
+(*
let branch_relation storage : Relation.t =
let schema =
Schema.empty |> Schema.add "name" "string" |> Schema.add "hash" "string"
@@ -102,9 +102,9 @@ module Make (Storage : Physical.S with type error = string) = struct
~generator:(Some generator)
~membership_criteria:(fun _ _ -> true)
~provenance:(Relation.Provenance.Base Prelude.Catalog.branch_rel_name)
- ~lineage:(Relation.Lineage.Base Prelude.Catalog.branch_rel_name) ()
+ ~lineage:(Relation.Lineage.Base Prelude.Catalog.branch_rel_name) () *)
- let head_relation storage : Relation.t =
+ (* let head_relation storage : Relation.t =
let schema = Schema.empty |> Schema.add "branch" "string" in
let mk v = { Attribute.value = Obj.repr v } in
let generator position =
@@ -129,5 +129,5 @@ module Make (Storage : Physical.S with type error = string) = struct
~generator:(Some generator)
~membership_criteria:(fun _ _ -> true)
~provenance:(Relation.Provenance.Base Prelude.Catalog.head_rel_name)
- ~lineage:(Relation.Lineage.Base Prelude.Catalog.head_rel_name) ()
+ ~lineage:(Relation.Lineage.Base Prelude.Catalog.head_rel_name) () *)
end
diff --git a/lib/management/database.ml b/lib/management/database.ml
deleted file mode 100644
index b8a9829..0000000
--- a/lib/management/database.ml
+++ /dev/null
@@ -1,136 +0,0 @@
-(** Database state: a named collection of relations with a Merkle tree over
- their hashes, a domain registry, and a history chain of prior hashes. All
- mutations produce a new value; no in-place update. *)
-
-(* TODO: history is an unbounded list of hashes; there is no way to
- reconstruct a past state from a hash alone without a separate snapshot
- store. Either cap the history or make it actually useful. *)
-
-(* TODO: refactor RelationMap keying from composite "schema:relation" strings
- to nested maps (schema -> relation -> Relation.t) for cleaner semantics.
- Currently, callers are responsible for passing composite keys. *)
-module RelationMap = Map.Make (String)
-
-type deferred_entry = {
- relation_name : string;
- constraint_name : string;
- body : Constraint.t;
-}
-
-type t = {
- hash : Conventions.Hash.t;
- name : Conventions.Name.t;
- tree : Merkle.t;
- relations : Relation.t RelationMap.t;
- domains : Domain.t RelationMap.t;
- history : Conventions.Hash.t list;
- timestamp : float;
- deferred : deferred_entry list;
-}
-
-let empty ~name =
- {
- hash = "";
- name;
- tree = Merkle.empty;
- relations = RelationMap.empty;
- domains = RelationMap.empty;
- history = [];
- timestamp = Unix.gettimeofday ();
- deferred = [];
- }
-
-let compute_hash db =
- match Merkle.root_hash db.tree with
- | Some root -> root
- | None -> Conventions.Hash.hash_text db.name
-
-let max_history = 128
-
-let rec take_at_most n = function
- | [] -> []
- | _ when n = 0 -> []
- | x :: xs -> x :: take_at_most (n - 1) xs
-
-let update_state db ~relations ~tree =
- let new_hash =
- match Merkle.root_hash tree with
- | Some root -> root
- | None -> Conventions.Hash.hash_text db.name
- in
- let history =
- if db.hash = "" then db.history
- else take_at_most max_history (db.hash :: db.history)
- in
- {
- hash = new_hash;
- name = db.name;
- tree;
- relations;
- domains = db.domains;
- history;
- timestamp = Unix.gettimeofday ();
- deferred = db.deferred;
- }
-
-let get_relation db name = RelationMap.find_opt name db.relations
-
-let get_relation_hash db name =
- match RelationMap.find_opt name db.relations with
- | None -> None
- | Some rel -> rel.Relation.hash
-
-let get_relation_names db =
- RelationMap.fold (fun name _ acc -> name :: acc) db.relations []
-
-let has_relation db name = RelationMap.mem name db.relations
-
-let relation_hash_or_compute (relation : Relation.t) =
- match relation.hash with
- | Some h -> h
- | None ->
- let tree = Option.value relation.tree ~default:Merkle.empty in
- Hashing.hash_relation ~name:relation.name ~schema:relation.schema ~tree
-
-(* TODO: computing the hash here for hash:None relations (ephemeral/prelude)
- is a layering violation — callers should assign the hash before calling
- add_relation, not rely on this fallback. *)
-let add_relation db ~(relation : Relation.t) =
- let relation_hash = relation_hash_or_compute relation in
- let relation = { relation with hash = Some relation_hash } in
- let tree = Merkle.insert relation_hash db.tree in
- let relations = RelationMap.add relation.name relation db.relations in
- update_state db ~relations ~tree
-
-let remove_relation db ~name =
- match RelationMap.find_opt name db.relations with
- | None -> db
- | Some relation ->
- let relation_hash = relation_hash_or_compute relation in
- let tree = Merkle.delete relation_hash db.tree in
- let relations = RelationMap.remove name db.relations in
- update_state db ~relations ~tree
-
-let update_relation db ~(relation : Relation.t) =
- let name = relation.name in
- match RelationMap.find_opt name db.relations with
- | None -> db
- | Some old_relation ->
- let old_hash = relation_hash_or_compute old_relation in
- let new_hash = relation_hash_or_compute relation in
- let tree = db.tree |> Merkle.delete old_hash |> Merkle.insert new_hash in
- let relations = RelationMap.add name relation db.relations in
- update_state db ~relations ~tree
-
-let add_domain db (domain : Domain.t) =
- { db with domains = RelationMap.add domain.name domain db.domains }
-
-let get_domain db name = RelationMap.find_opt name db.domains
-
-let get_domain_names db =
- RelationMap.fold (fun name _ acc -> name :: acc) db.domains []
-
-let has_domain db name = RelationMap.mem name db.domains
-
-let remove_domain db name =
- { db with domains = RelationMap.remove name db.domains }
diff --git a/lib/management/diff.ml b/lib/management/diff.ml
deleted file mode 100644
index 8fca0af..0000000
--- a/lib/management/diff.ml
+++ /dev/null
@@ -1,70 +0,0 @@
-(** Structural delta between two Database.t states. Computes what changed from
- ancestor -> target at the relation level. *)
-
-type relation_diff =
- | RelationAdded of Relation.t
- | RelationRemoved of string
- | RelationModified of {
- name : string;
- added_tuples : Conventions.Hash.t list;
- removed_tuples : Conventions.Hash.t list;
- schema_changed : bool;
- }
-
-type t = {
- ancestor : Conventions.Hash.t;
- left : Conventions.Hash.t;
- right : Conventions.Hash.t;
- relation_diffs : relation_diff list;
-}
-
-module StringSet = Set.Make (String)
-
-let relation_keys rel =
- match rel.Relation.tree with
- | None -> StringSet.empty
- | Some tree -> StringSet.of_list (Merkle.keys tree)
-
-(** Compute relation-level diffs from ancestor → target. *)
-let diff ~ancestor ~(target : Database.t) =
- let ancestor_names =
- Database.get_relation_names ancestor |> StringSet.of_list
- in
- let target_names = Database.get_relation_names target |> StringSet.of_list in
- let all_names = StringSet.union ancestor_names target_names in
- StringSet.fold
- (fun name acc ->
- let in_ancestor = StringSet.mem name ancestor_names in
- let in_target = StringSet.mem name target_names in
- match (in_ancestor, in_target) with
- | false, true -> (
- match Database.get_relation target name with
- | Some rel -> RelationAdded rel :: acc
- | None -> acc)
- | true, false -> RelationRemoved name :: acc
- | true, true -> (
- match
- ( Database.get_relation ancestor name,
- Database.get_relation target name )
- with
- | Some anc_rel, Some tgt_rel ->
- (* Compare hashes — if identical, no diff *)
- if anc_rel.Relation.hash = tgt_rel.Relation.hash then acc
- else
- let anc_keys = relation_keys anc_rel in
- let tgt_keys = relation_keys tgt_rel in
- let added_tuples =
- StringSet.diff tgt_keys anc_keys |> StringSet.elements
- in
- let removed_tuples =
- StringSet.diff anc_keys tgt_keys |> StringSet.elements
- in
- let schema_changed =
- anc_rel.Relation.schema <> tgt_rel.Relation.schema
- in
- RelationModified
- { name; added_tuples; removed_tuples; schema_changed }
- :: acc
- | _ -> acc)
- | false, false -> acc (* impossible *))
- all_names []
diff --git a/lib/management/merge.ml b/lib/management/merge.ml
deleted file mode 100644
index dff4808..0000000
--- a/lib/management/merge.ml
+++ /dev/null
@@ -1,288 +0,0 @@
-(** 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
- | ConstraintConflict of string
-
-type strategy = PreferLeft | PreferRight | RevertToAncestor
-type merge_result = Clean of Database.t | Failed of conflict list
-
-(* TODO: Remove this later. We already should have the interface on the manipulation *)
-
-(** 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 store_database : storage -> Database.t -> (unit, error) result
-end
-
-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 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. *)
-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
- { 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))
-
-module Make
- (Storage : Physical.S)
- (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,
- lookup for right diffs by relation name, start from ancestor and
- apply all the diffs, apply first the left diffs, then right-only diffs
- (relations only right touched). If there are no conflicts, persist the
- merged database, otherwise return Failed, unless a strategy resolved them. *)
- let merge ~storage ~strategy ~left_tip ~right_tip =
- 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)))
-end
diff --git a/lib/management/multigroup.ml b/lib/management/multigroup.ml
new file mode 100644
index 0000000..cfde4c1
--- /dev/null
+++ b/lib/management/multigroup.ml
@@ -0,0 +1,84 @@
+(** Multigroup root: a lightweight versioned descriptor that maps relation names
+ to content-addressed relation values. The multigroup object owns the B+ tree
+ pointer and advances it when relations change. *)
+
+
+class multigroup ~name:(init_name : Conventions.Name.t) =
+ let init_hash = Conventions.Hash.hash_text init_name in
+ object (self)
+ val name : Conventions.Name.t = init_name
+ val hash : Conventions.Hash.t = init_hash
+ val previous_version : Conventions.Hash.t option = None
+ val tree_pointer : Conventions.Hash.t option = None
+ val relations : Relation.relation BatMap.String.t = BatMap.String.empty
+ val timestamp : float = Unix.gettimeofday ()
+
+ method name = name
+ method hash = hash
+ method previous_version = previous_version
+ method tree_pointer = tree_pointer
+ method relations = relations
+ method timestamp = timestamp
+
+ method get_relation rel_name = BatMap.String.find_opt rel_name relations
+ method get_relation_names = BatMap.String.fold (fun n _ acc -> n :: acc) relations []
+ method has_relation rel_name = BatMap.String.mem rel_name relations
+
+ (* Derive the B+ tree root hash from sorted relation hashes. *)
+ method private compute_tree_pointer rels =
+ BatMap.String.fold
+ (fun _n (r : Relation.relation) acc -> acc ^ r#hash)
+ rels ""
+ |> Conventions.Hash.hash_text
+
+ method private compute_hash tp =
+ Conventions.Hash.hash_text (name ^ tp)
+
+ method private advance rels =
+ let tp = self#compute_tree_pointer rels in
+ {< hash = self#compute_hash tp;
+ previous_version = (if hash = "" then None else Some hash);
+ tree_pointer = Some tp;
+ relations = rels;
+ timestamp = Unix.gettimeofday () >}
+
+ method add_relation (rel : Relation.relation) =
+ let rels = BatMap.String.add rel#name rel relations in
+ self#advance rels
+
+ method remove_relation rel_name =
+ if not (BatMap.String.mem rel_name relations) then self
+ else self#advance (BatMap.String.remove rel_name relations)
+
+ method update_relation (rel : Relation.relation) =
+ if not (BatMap.String.mem rel#name relations) then self
+ else self#advance (BatMap.String.add rel#name rel relations)
+
+ method serialize () : Storable.Multigroup.t =
+ {
+ Storable.Multigroup.name = name;
+ relations = BatMap.String.bindings relations |> List.map snd |> List.map (fun r -> r#serialize ());
+ }
+end
+
+let deserialize (_bytes : bytes) : (multigroup, string) result =
+ try
+ let stored = Storable.Multigroup.of_bytes _bytes in
+ let db = new multigroup ~name:stored.name in
+ let db =
+ List.fold_left
+ (fun db rel ->
+ let membership_criteria =
+ Constraint.build_membership_criteria ~_name:rel.Storable.Relation.name
+ ~schema:rel.schema
+ in
+ let relation =
+ new Relation.stored ~name:rel.name ~schema:rel.schema ~constraints:None
+ ~cardinality:Conventions.Cardinality.ConstrainedFinite
+ ~membership_criteria ~lineage:None ~provenance:None
+ in
+ db#add_relation relation)
+ db stored.relations
+ in
+ Ok db
+ with Invalid_argument message -> Error message