(** 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.). 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 include Configuration.CONFIGURABLE type connection val connect : configuration -> (connection, Condition.t) result (** Connect to the storage backend *) val disconnect : connection -> unit (** Disconnect from the backend *) val get : connection -> Conventions.Hash.t -> (bytes option, Condition.t) result (** Get a value by its hash. Returns None if not found. *) val put : connection -> Conventions.Hash.t -> bytes -> (unit, Condition.t) result (** Store a value at its hash *) val exists : connection -> Conventions.Hash.t -> (bool, Condition.t) result (** Check if a hash exists *) val begin_transaction : connection -> (unit, Condition.t) result (** Begin a transaction *) val commit : connection -> (unit, Condition.t) result (** Commit a transaction *) val rollback : connection -> (unit, Condition.t) result (** Rollback a transaction *) val get_many : connection -> Conventions.Hash.t list -> (bytes option list, Condition.t) result (** Batch get multiple values *) val put_many : connection -> (Conventions.Hash.t * bytes) list -> (unit, Condition.t) result (** Batch put multiple values *) end (** Storage operations built on top of a backend *) module type S = sig include Configuration.CONFIGURABLE type t val create : configuration -> (t, Condition.t) result (** Create a storage instance from a backend connection *) val close : t -> unit (** Close the storage *) val store_attribute : t -> Conventions.AbstractValue.t -> (Conventions.Hash.t, Condition.t) result (** Store an attribute value, returns its hash *) val load_attribute : t -> Conventions.Hash.t -> (Conventions.AbstractValue.t option, Condition.t) result (** Load an attribute value by hash *) val store_raw : t -> Conventions.Hash.t -> bytes -> (unit, Condition.t) result (** Store raw bytes at a hash *) val load_raw : t -> Conventions.Hash.t -> (bytes option, Condition.t) result (** Load raw bytes by hash *) val exists : t -> Conventions.Hash.t -> (bool, Condition.t) result (** Check if a hash exists *) val with_transaction : t -> (unit -> ('a, Condition.t) result) -> ('a, Condition.t) result (** Execute a function within a transaction *) end (** Functor to create storage operations from a backend *) module Make (B : BACKEND) : S with type configuration = B.configuration = struct type configuration = B.configuration type t = B.connection let parse = B.parse let create config = B.connect config 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 let load_attribute conn hash = match B.get conn hash with | Ok (Some bytes) -> Ok (Some (Marshal.from_bytes bytes 0)) | 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 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) end (** In-memory backend for testing and development *) module MemoryBackend : BACKEND with type configuration = unit = struct type configuration = unit type connection = { data : (string, bytes) Hashtbl.t; mutable in_transaction : bool; mutable transaction_buffer : (string * bytes) list; } module Error = struct open Condition (* TODO: more structure *) let parse_error msg = condition "parse-error" msg empty let already_in_transaction = condition "already-in-transaction" "Already inside a transaction" empty let not_in_transaction = condition "not-in-transaction" "Not inside a transaction" empty end let parse sexp = match sexp with | Sexplib.Sexp.List [] -> Ok () | _ -> Error (Error.parse_error (Printf.sprintf "memory backend takes no configuration, got: %s" (Sexplib.Sexp.to_string sexp))) let connect () = Ok { data = Hashtbl.create 1024; in_transaction = false; transaction_buffer = []; } let disconnect _ = () 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 Hashtbl.replace conn.data hash value; Ok () end let exists conn hash = Ok (Hashtbl.mem conn.data hash) let begin_transaction conn = if conn.in_transaction then Error Error.already_in_transaction else begin conn.in_transaction <- true; conn.transaction_buffer <- []; Ok () end let commit conn = if not conn.in_transaction then Error Error.not_in_transaction else begin 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 Error.not_in_transaction else begin conn.in_transaction <- false; conn.transaction_buffer <- []; Ok () end let get_many conn hashes = 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; Ok () end module Memory = Make (MemoryBackend) (** Default in-memory storage for development *)