module Wire = struct let split_once ch value = match String.index_opt value ch with | None -> None | Some index -> Some (String.sub value 0 index, String.sub value (index + 1) (String.length value - index - 1)) let lines bytes = bytes |> Bytes.to_string |> String.split_on_char '\n' |> List.filter (fun line -> String.trim line <> "") let fields bytes = lines bytes |> List.filter_map (fun line -> split_once '=' line) |> List.map (fun (key, value) -> String.trim key, String.trim value) let require_type expected fields = match List.assoc_opt "type" fields with | Some actual when actual = expected -> () | Some actual -> invalid_arg (Printf.sprintf "expected storable type %S, got %S" expected actual) | None -> invalid_arg "missing storable type" let values key fields = List.filter_map (fun (k, v) -> if k = key then Some v else None) fields let required key fields = match List.assoc_opt key fields with | Some value -> value | None -> invalid_arg ("missing storable field: " ^ key) let attribute value = match split_once ':' value with | Some pair -> pair | None -> invalid_arg ("invalid attribute field: " ^ value) let encode fields = fields |> List.map (fun (key, value) -> key ^ "=" ^ value) |> String.concat "\n" |> fun value -> Bytes.of_string (value ^ "\n") end module Tuple = struct type t = {relation: string; attributes: (string * string) list} let to_bytes tuple = Wire.encode ( ["type", "tuple"; "relation", tuple.relation] @ List.map (fun (name, value) -> "attribute", name ^ ":" ^ value) tuple.attributes ) let of_bytes bytes = let fields = Wire.fields bytes in Wire.require_type "tuple" fields; { relation= Wire.required "relation" fields; attributes= List.map Wire.attribute (Wire.values "attribute" fields) } end module Relation = struct type t = { name: string; schema: (string * string) list; (** Hex Merkle root of the relation's tuple set. Empty string = empty relation. *) tree_pointer: string } let to_bytes relation = Wire.encode ( [ "type", "relation"; "name", relation.name; "kind", "stored"; "tree_pointer", relation.tree_pointer ] @ List.map (fun (name, domain) -> "attribute", name ^ ":" ^ domain) relation.schema ) let of_bytes bytes = let fields = Wire.fields bytes in Wire.require_type "relation" fields; { name= Wire.required "name" fields; schema= List.map Wire.attribute (Wire.values "attribute" fields); tree_pointer= Option.value (List.assoc_opt "tree_pointer" fields) ~default:"" } end module Multigroup = struct type t = {name: string; relations: Relation.t list} (* Wire format: "name|attr1:dom1,attr2:dom2|" The third segment is the Merkle root hex; empty string = empty relation. A fourth segment for constraints is reserved but not yet implemented. TODO: add constraints serialisation once a canonical sexp representation is agreed upon (two syntactically distinct bodies may be semantically equivalent — the round-trip must account for that). *) let relation_value relation = let attrs = relation.Relation.schema |> List.map (fun (name, domain) -> name ^ ":" ^ domain) |> String.concat "," in relation.name ^ "|" ^ attrs ^ "|" ^ relation.tree_pointer let parse_relation value = match Wire.split_once '|' value with | None -> {Relation.name= value; schema= []; tree_pointer= ""} | Some (name, rest) -> let attrs_str, tree_pointer = match Wire.split_once '|' rest with None -> rest, "" | Some (a, tp) -> a, tp in let schema = if attrs_str = "" then [] else attrs_str |> String.split_on_char ',' |> List.map Wire.attribute in {Relation.name; schema; tree_pointer} let to_bytes multigroup = Wire.encode ( ["type", "multigroup"; "name", multigroup.name] @ List.map (fun relation -> "relation", relation_value relation) multigroup.relations ) let of_bytes bytes = let fields = Wire.fields bytes in Wire.require_type "multigroup" fields; { name= Wire.required "name" fields; relations= List.map parse_relation (Wire.values "relation" fields) } end