blob: b8a9829a42f73487b2c188dfb78a19f43bfac765 (
plain) (
tree)
|
|
(** 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 }
|