aboutsummaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--lib/Utilities.ml4
-rw-r--r--lib/Utilities.mli4
-rw-r--r--lib/abstract/Storage.ml23
-rw-r--r--lib/backend/execution/FOL.ml181
-rw-r--r--lib/backend/managers/Cursor.ml18
-rw-r--r--lib/backend/managers/Cursor.mli16
-rw-r--r--lib/backend/managers/Handle.ml51
-rw-r--r--lib/backend/managers/Handle.mli22
-rw-r--r--lib/backend/storage/LMDB.ml175
-rw-r--r--lib/backend/storage/LMDB.mli2
-rw-r--r--lib/concepts/Condition.ml29
-rw-r--r--lib/concepts/Condition.mli14
-rw-r--r--lib/concepts/Hash.ml8
-rw-r--r--lib/concepts/Hash.mli13
-rw-r--r--lib/concepts/Path.ml16
-rw-r--r--lib/concepts/Path.mli8
-rw-r--r--lib/concepts/Representation.ml7
-rw-r--r--lib/concepts/Representation.mli13
-rw-r--r--lib/concepts/Tuple.ml46
-rw-r--r--lib/concepts/Tuple.mli9
-rw-r--r--lib/concepts/Value.ml6
-rw-r--r--lib/concepts/Value.mli9
-rw-r--r--lib/dune4
23 files changed, 551 insertions, 127 deletions
diff --git a/lib/Utilities.ml b/lib/Utilities.ml
new file mode 100644
index 0000000..bd604c9
--- /dev/null
+++ b/lib/Utilities.ml
@@ -0,0 +1,4 @@
+module Result = struct
+ let ( let* ) = Result.bind
+ let fmap f m = Result.bind m f
+end
diff --git a/lib/Utilities.mli b/lib/Utilities.mli
new file mode 100644
index 0000000..c9852f0
--- /dev/null
+++ b/lib/Utilities.mli
@@ -0,0 +1,4 @@
+module Result : sig
+ val ( let* ) : ('a, 'e) result -> ('a -> ('b, 'e) result) -> ('b, 'e) result
+ val fmap : ('a -> ('b, 'e) result) -> ('a, 'e) result -> ('b, 'e) result
+end
diff --git a/lib/abstract/Storage.ml b/lib/abstract/Storage.ml
index d0f2097..f456567 100644
--- a/lib/abstract/Storage.ml
+++ b/lib/abstract/Storage.ml
@@ -2,17 +2,28 @@ module type STORAGE = sig
type connection
type transaction
- val connect: Concepts.Configuration.configuration -> connection
+ val connect :
+ Concepts.Configuration.configuration -> (connection, Concepts.Condition.condition) result
(** begin a transaction within `connection` *)
- val start: connection -> transaction
+ val start : connection -> (transaction, Concepts.Condition.condition) result
+
(** `commit` the current transaction *)
- val commit: transaction -> ()
+ val commit : transaction -> (unit, Concepts.Condition.condition) result
+
(** `abort` the current transaction *)
- val abort: transaction -> ()
+ val abort : transaction -> (unit, Concepts.Condition.condition) result
(** within a transaction, fetch a blob from disk *)
- val get: transaction -> Concepts.Hash.hash -> Concepts.Representation.blob
+ val get :
+ transaction ->
+ Concepts.Hash.hash ->
+ (Concepts.Representation.blob option, Concepts.Condition.condition) result
+
(** within a transaction, associate a hash with a blob on disk *)
- val put: transaction -> Concepts.Hash.hash -> Concepts.Representation.blob -> ()
+ val put :
+ transaction ->
+ Concepts.Hash.hash ->
+ Concepts.Representation.blob ->
+ (unit, Concepts.Condition.condition) result
end
diff --git a/lib/backend/execution/FOL.ml b/lib/backend/execution/FOL.ml
index 1a5770e..302285d 100644
--- a/lib/backend/execution/FOL.ml
+++ b/lib/backend/execution/FOL.ml
@@ -1,69 +1,9 @@
-(** A plan is pure data ([Plan.t]) and execution state is never
- reified: a compiled plan is a [Seq.t], whose forcing is the
- Volcano eruption [Next]
-
- The only effects sit at scan leaves, and those are idempotent page
- reads against a pinned [Multigroup] snapshot. A stream node may be
- forced twice and observe the same tuples. If a truly destructive
- source ever appears, wrap its stream in [Seq.once]. *)
-
-module Tuple = struct
- module AttributeMap = BatMap.String
-
- (* TODO: The assumption here is that every value is stringified *)
- (* TODO: The field [type'] currently is not enforced as all the
- available types of RNT *)
- type attribute = {value: string; type': string}
- type t = attribute AttributeMap.t
-
- let empty : t = AttributeMap.empty
- let access (name : string) (t : t) : attribute option = AttributeMap.find_opt name t
-
- let merge (left : t) (right : t) : t =
- let merger _ (left : attribute option) (right : attribute option) : attribute option =
- match left, right with
- | Some a, Some b -> if a = b then Some a else None (* conflicting binding gets droped out *)
- | (Some _ as a), None | None, (Some _ as a) -> a
- | None, None -> None
- in
- AttributeMap.merge merger left right
-
- let project (keep : BatSet.String.t) (t : t) : t =
- AttributeMap.filter (fun name _ -> BatSet.String.mem name keep) t
-
- let rename (mapping : string BatMap.String.t) (t : t) : t =
- AttributeMap.fold
- (fun name attr acc ->
- let name = BatMap.String.find_default name name mapping in
- AttributeMap.add name attr acc )
- t AttributeMap.empty
-end
-
-(** Stub of the cursor layer (CursorManager + Merkle paging). [page]
- yields the tuples of [relation] starting at [offset]; an empty
- result marks exhaustion. [args] carries the bound values for
- parameterized (ephemeral) relations and is ignored for stored
- ones. *)
-module Source = struct
- type t =
- {page: relation:string -> args:string BatFingerTree.t -> offset:int -> Tuple.t BatFingerTree.t}
-
- let unimplemented : t = {page= (fun ~relation:_ ~args:_ ~offset:_ -> failwith "NOT IMPLEMENTED")}
-end
-
-(** The stateless operator tree representing one constructor per
- relational operator, carrying only its description without any
- associated cursors, counters and buffers. *)
module Plan = struct
- (** One segment of a parameterized relation path in a SCAN. [Var]
- segments are resolved from the enclosing JOIN's outer tuple at
- execution time; [Const] segments are ground literals fixed at
- the plan construction. *)
- type path_arg = Var of string | Const of string
+ type path_arg = Var of string | Const of Concepts.Value.value
type t =
- | Scan of {relation: string; args: path_arg BatFingerTree.t}
- | Join of {left: t; right: t; attrs: BatSet.String.t}
+ | Scan of {path: Concepts.Path.t; args: path_arg BatFingerTree.t}
+ | Natural of {left: t; right: t}
| Take of {limit: int; from: t}
| Project of {attrs: BatSet.String.t; from: t}
| Materialize of t
@@ -71,34 +11,95 @@ module Plan = struct
| Union of t BatFingerTree.t
end
-(** Compiles a [Plan.t] into a demand driven tuple stream. *)
-module FOL = struct
- type stream = Tuple.t BatSeq.t
+type stream = (Concepts.Tuple.t, Concepts.Condition.condition) result BatSeq.t
+
+let singleton (x : ('a, 'e) result) : ('a, 'e) result BatSeq.t =
+ fun () -> BatSeq.Cons (x, BatSeq.empty)
- (** Resolves a scan arguments template against the enclosing
- bindings. *)
- (* TODO: A [Var] missing from the bindings resolves to [""] to
- mirror the C++ engine. This is a poor representation in the type
- system. *)
- let resolve (args : Plan.path_arg BatFingerTree.t) (bindings : Tuple.t) : string BatFingerTree.t =
- BatFingerTree.map
- (function
- | Plan.Const value -> value
- | Plan.Var name -> (match Tuple.access name bindings with Some a -> a.value | None -> "" ))
- args
+module Error = struct
+ open Concepts.Condition
- (** Pages through [relation] one fetch at a time. The offset lives
- in the closure of each stream node, so re-forcing any node
- re-reads the same page. This is the functional equivalent of the
- cursor's [fetch_offset]. Only the current page should be
- live. *)
- let scan (src : Source.t) ~(relation : string) ~(args : string BatFingerTree.t) : stream =
- let rec from offset () =
- let page = src.page ~relation ~args ~offset in
- if BatFingerTree.is_empty page then BatSeq.Nil
- else
- let rest = from (offset + BatFingerTree.size page) in
- (BatFingerTree.fold_right (fun rest tuple () -> BatSeq.Cons (tuple, rest)) rest page) ()
+ let unbound_variable name =
+ condition "unbound-variable"
+ (Printf.sprintf "path variable %S is not bound in the enclosing tuple" name)
+ ("variable" |=| Concepts.Value.String name)
+end
+
+let resolve (args : Plan.path_arg BatFingerTree.t) (bindings : Concepts.Tuple.t) :
+ (Concepts.Value.value BatFingerTree.t, Concepts.Condition.condition) result =
+ let open Utilities.Result in
+ BatFingerTree.fold_left
+ (fun acc arg ->
+ let* acc = acc in
+ let* value =
+ match arg with
+ | Plan.Const value -> Ok value
+ | Plan.Var name ->
+ Option.to_result ~none:(Error.unbound_variable name)
+ (Concepts.Tuple.access name bindings)
+ in
+ Ok (BatFingerTree.snoc acc value) )
+ (Ok BatFingerTree.empty) args
+
+module Make (Handler : Managers.Handle.HANDLER) = struct
+ let scan handler ~path ~args : stream =
+ let opened =
+ let open Utilities.Result in
+ let* handle = Handler.open_ handler ~path in
+ Handler.open_cursor handle ~args
in
- from 0
+ match opened with
+ | Error condition -> singleton (Error condition)
+ | Ok cursor ->
+ let rec pull () =
+ match Handler.next cursor with
+ | Error condition -> BatSeq.Cons (Error condition, BatSeq.empty)
+ | Ok None -> BatSeq.Nil
+ | Ok (Some tuple) -> BatSeq.Cons (Ok tuple, pull)
+ in
+ pull
+
+ let rec compile handler : Plan.t -> Concepts.Tuple.t -> stream = function
+ | Scan {path; args} -> (
+ fun bindings ->
+ match resolve args bindings with
+ | Ok args -> scan handler ~path ~args
+ | Error condition -> singleton (Error condition) )
+ | Project {attrs; from} ->
+ let from = compile handler from in
+ fun bindings -> BatSeq.map (Result.map (Concepts.Tuple.project attrs)) (from bindings)
+ | Rename {attrs; from} ->
+ let from = compile handler from in
+ fun bindings -> BatSeq.map (Result.map (Concepts.Tuple.rename attrs)) (from bindings)
+ | Take {limit; from} ->
+ let from = compile handler from in
+ fun bindings -> BatSeq.take limit (from bindings)
+ | Union plans ->
+ let compiled = BatFingerTree.map (compile handler) plans in
+ fun bindings ->
+ BatFingerTree.fold_right
+ (fun rest c -> BatSeq.append (c bindings) rest)
+ BatSeq.empty compiled
+ | Natural {left; right} ->
+ let left = compile handler left and right = compile handler right in
+ let matches l r =
+ BatMap.String.for_all
+ (fun name value ->
+ match Concepts.Tuple.access name r with Some value' -> value = value' | None -> true )
+ l
+ in
+ fun bindings ->
+ left bindings
+ |> BatSeq.concat_map (function
+ | Error _ as error -> singleton error
+ | Ok l ->
+ right l
+ |> BatSeq.filter (function Ok r -> matches l r | Error _ -> true)
+ |> BatSeq.map (Result.map (Concepts.Tuple.merge l)) )
+ | Materialize from ->
+ let cached = lazy (BatSeq.memoize (compile handler from Concepts.Tuple.empty)) in
+ fun _ -> Lazy.force cached
+
+ (* TODO: This should produce ephemeral relations with support of the sublanguage to manage it. *)
+ let run handler plan : stream = compile handler plan Concepts.Tuple.empty
end
diff --git a/lib/backend/managers/Cursor.ml b/lib/backend/managers/Cursor.ml
new file mode 100644
index 0000000..3295cae
--- /dev/null
+++ b/lib/backend/managers/Cursor.ml
@@ -0,0 +1,18 @@
+type descriptor =
+ | Stored of {merkle_root: string}
+ | Ephemeral of {merkle_root: string; dependencies: string BatFingerTree.t}
+
+module Make (Store : Abstract.Storage.STORAGE) = struct
+ type cursor = unit
+
+ let open_ (_txn : Store.transaction) (descriptor : descriptor)
+ ~(args : Concepts.Value.value BatFingerTree.t) : (cursor, Concepts.Condition.condition) result
+ =
+ ignore (descriptor, args);
+ failwith "NOT IMPLEMENTED: open a cursor by paging the relation's Merkle tree over Store.get"
+
+ let next (_cursor : cursor) : (Concepts.Tuple.t option, Concepts.Condition.condition) result =
+ failwith "NOT IMPLEMENTED: page the next tuple over Store.get"
+
+ let close (_cursor : cursor) : unit = ()
+end
diff --git a/lib/backend/managers/Cursor.mli b/lib/backend/managers/Cursor.mli
new file mode 100644
index 0000000..71fb15d
--- /dev/null
+++ b/lib/backend/managers/Cursor.mli
@@ -0,0 +1,16 @@
+type descriptor =
+ | Stored of {merkle_root: string}
+ | Ephemeral of {merkle_root: string; dependencies: string BatFingerTree.t}
+
+module Make (Store : Abstract.Storage.STORAGE) : sig
+ type cursor
+
+ val open_ :
+ Store.transaction ->
+ descriptor ->
+ args:Concepts.Value.value BatFingerTree.t ->
+ (cursor, Concepts.Condition.condition) result
+
+ val next : cursor -> (Concepts.Tuple.t option, Concepts.Condition.condition) result
+ val close : cursor -> unit
+end
diff --git a/lib/backend/managers/Handle.ml b/lib/backend/managers/Handle.ml
new file mode 100644
index 0000000..ed2f220
--- /dev/null
+++ b/lib/backend/managers/Handle.ml
@@ -0,0 +1,51 @@
+module type HANDLER = sig
+ type t
+ type handle
+ type cursor
+
+ val open_ : t -> path:Concepts.Path.t -> (handle, Concepts.Condition.condition) result
+
+ val open_cursor :
+ handle ->
+ args:Concepts.Value.value BatFingerTree.t ->
+ (cursor, Concepts.Condition.condition) result
+
+ val next : cursor -> (Concepts.Tuple.t option, Concepts.Condition.condition) result
+ val close : cursor -> unit
+end
+
+let not_a_relation () : Concepts.Condition.condition =
+ Concepts.Condition.condition "not-a-relation"
+ "handle does not refer to a stored or ephemeral relation" Concepts.Condition.empty
+
+module Make (Store : Abstract.Storage.STORAGE) = struct
+ module Cur = Cursor.Make (Store)
+
+ type t = {objects: Object.rnt_object_tree; txn: Store.transaction}
+ type handle = {object_: Object.registry; txn: Store.transaction}
+ type cursor = Cur.cursor
+
+ let create objects txn = {objects; txn}
+
+ let open_ t ~path =
+ let object_ = Object.find path t.objects in
+ Ok {object_; txn= t.txn}
+
+ let relation_descriptor (object_ : Object.registry) :
+ (Cursor.descriptor, Concepts.Condition.condition) result =
+ let {Object.entry; _} = object_ in
+ match entry.Object.kind with
+ | Object.Relation {merkle_root} -> Ok (Cursor.Stored {merkle_root})
+ | Object.EphemeralRelation {merkle_root; dependencies} ->
+ Ok (Cursor.Ephemeral {merkle_root; dependencies})
+ | _ -> Error (not_a_relation ())
+
+ let open_cursor handle ~args =
+ let open Utilities.Result in
+ let* descriptor = relation_descriptor handle.object_ in
+ Cur.open_ handle.txn descriptor ~args
+
+ let next = Cur.next
+ let close = Cur.close
+ let object_ (handle : handle) = handle.object_
+end
diff --git a/lib/backend/managers/Handle.mli b/lib/backend/managers/Handle.mli
new file mode 100644
index 0000000..5fc82b8
--- /dev/null
+++ b/lib/backend/managers/Handle.mli
@@ -0,0 +1,22 @@
+module type HANDLER = sig
+ type t
+ type handle
+ type cursor
+
+ val open_ : t -> path:Concepts.Path.t -> (handle, Concepts.Condition.condition) result
+
+ val open_cursor :
+ handle ->
+ args:Concepts.Value.value BatFingerTree.t ->
+ (cursor, Concepts.Condition.condition) result
+
+ val next : cursor -> (Concepts.Tuple.t option, Concepts.Condition.condition) result
+ val close : cursor -> unit
+end
+
+module Make (Store : Abstract.Storage.STORAGE) : sig
+ include HANDLER
+
+ val create : Object.rnt_object_tree -> Store.transaction -> t
+ val object_ : handle -> Object.registry
+end
diff --git a/lib/backend/storage/LMDB.ml b/lib/backend/storage/LMDB.ml
index 3478da2..f4fcea1 100644
--- a/lib/backend/storage/LMDB.ml
+++ b/lib/backend/storage/LMDB.ml
@@ -1,11 +1,170 @@
-type connection = unit
-type transaction = unit
+module C = struct
+ open Ctypes
+ open PosixTypes
+ open Foreign
-let connect (_: Concepts.Configuration.configuration) = failwith "TODO"
+ let mdb_result_of_int = function 0 -> Ok () | x -> Error x
+ let int_of_mdb_result = function Ok _ -> 0 | Error x -> x
+ let mdb_mode_t = mode_t
+ let mdb_result = view ~read:mdb_result_of_int ~write:int_of_mdb_result int
-let start (_: connection) = failwith "TODO"
-let commit (_: transaction) = failwith "TODO"
-let abort (_: transaction) = failwith "TODO"
+ type mdb_env
-let get (_: transaction) (_: Concepts.Hash.hash) = failwith "TODO"
-let put (_: transaction) (_: Concepts.Hash.hash) (_: Concepts.Representation.blob) = failwith "TODO"
+ let mdb_env : mdb_env structure typ = structure "mdb_env"
+
+ type mdb_dbi = Unsigned.uint
+
+ let mdb_dbi : mdb_dbi typ = uint
+
+ type mdb_txn
+
+ let mdb_txn : mdb_txn structure typ = structure "mdb_txn"
+
+ type mdb_val
+
+ let mdb_val : mdb_val structure typ = structure "mdb_val"
+ let mv_size = field mdb_val "mv_size" size_t
+ let mv_data = field mdb_val "mv_data" (ptr void)
+ let () = seal mdb_val
+
+ let with_output_pointer output_type default body =
+ let open Utilities.Result in
+ let output_ptr = allocate output_type default in
+ let* () = body output_ptr in
+ Ok !@output_ptr
+
+ let mdb_env_create = foreign "mdb_env_create" (ptr (ptr mdb_env) @-> returning mdb_result)
+
+ let mdb_env_create' () =
+ with_output_pointer (ptr mdb_env) (from_voidp mdb_env null) mdb_env_create
+
+ let mdb_env_open =
+ foreign "mdb_env_open" (ptr mdb_env @-> string @-> uint @-> mdb_mode_t @-> returning mdb_result)
+
+ let mdb_env_close = foreign "mdb_env_close" (ptr mdb_env @-> returning void)
+
+ let mdb_dbi_open =
+ foreign "mdb_dbi_open"
+ (ptr mdb_txn @-> ptr char @-> uint @-> ptr mdb_dbi @-> returning mdb_result)
+
+ let mdb_dbi_open' txn flags =
+ with_output_pointer mdb_dbi (Unsigned.UInt.of_int 0)
+ (mdb_dbi_open txn (from_voidp char null) flags)
+
+ let mdb_txn_begin =
+ foreign "mdb_txn_begin"
+ (ptr mdb_env @-> ptr mdb_txn @-> uint @-> ptr (ptr mdb_txn) @-> returning mdb_result)
+
+ let mdb_txn_begin' env parent flags = with_output_pointer
+ (ptr mdb_txn)
+ (from_voidp mdb_txn null)
+ (mdb_txn_begin env parent flags)
+ [@@ocamlformat "disable"]
+
+ let mdb_txn_commit = foreign "mdb_txn_commit" (ptr mdb_txn @-> returning mdb_result)
+ let mdb_txn_abort = foreign "mdb_txn_abort" (ptr mdb_txn @-> returning mdb_result)
+
+ (* TODO: make it so that we do not need to copy bytes to a separate array *)
+ let carray_of_bytes (b : bytes) =
+ let buffer = CArray.make char (Bytes.length b) in
+ Bytes.iteri (CArray.set buffer) b;
+ buffer
+
+ let bytes_of_carray arr =
+ let buffer = Bytes.make (CArray.length arr) (Char.chr 0) in
+ CArray.iteri (Bytes.set buffer) arr;
+ buffer
+
+ let mdb_val_ptr_of_bytes (b : bytes) =
+ let buf = carray_of_bytes b in
+ let s = make mdb_val in
+ setf s mv_size (Unsigned.Size_t.of_int (Bytes.length b));
+ setf s mv_data (to_voidp (CArray.start buf));
+ addr s
+
+ let bytes_of_mdb_val (s : mdb_val structure) =
+ let buf =
+ CArray.from_ptr (from_voidp char (getf s mv_data)) (Unsigned.Size_t.to_int (getf s mv_size))
+ in
+ bytes_of_carray buf
+
+ let mdb_get =
+ foreign "mdb_get"
+ (ptr mdb_txn @-> mdb_dbi @-> ptr mdb_val @-> ptr mdb_val @-> returning mdb_result)
+
+ let mdb_get' txn dbi key =
+ with_output_pointer mdb_val (make mdb_val) (mdb_get txn dbi (mdb_val_ptr_of_bytes key))
+ |> Result.map bytes_of_mdb_val
+
+ let mdb_put =
+ foreign "mdb_put"
+ (ptr mdb_txn @-> mdb_dbi @-> ptr mdb_val @-> ptr mdb_val @-> uint @-> returning mdb_result)
+
+ let mdb_put' txn dbi key data flags =
+ mdb_put txn dbi (mdb_val_ptr_of_bytes key) (mdb_val_ptr_of_bytes data) flags
+
+ let mdb_strerror = foreign "mdb_strerror" (int @-> returning string)
+
+ type mdb_env_ptr = mdb_env structure ptr
+ type mdb_txn_ptr = mdb_txn structure ptr
+
+ let null_txn = from_voidp mdb_txn null
+
+ module Errors = struct
+ let mdb_notfound = -30798
+ end
+end
+
+module Error = struct
+ open Concepts.Condition
+
+ let lmdb_error code =
+ condition "lmdb-error" (C.mdb_strerror code) ("code" |=| Concepts.Value.Integer code)
+end
+
+type connection = {env: C.mdb_env_ptr; dbi: C.mdb_dbi}
+type transaction = {tx: C.mdb_txn_ptr; dbi: C.mdb_dbi}
+
+let connect (_ : Concepts.Configuration.configuration) =
+ begin
+ let open Utilities.Result in
+ let* env = C.mdb_env_create' () in
+ (* TODO: make the path, POSIX mode a config parameter *)
+ match C.mdb_env_open env "/tmp/sakura.db" Unsigned.UInt.zero (PosixTypes.Mode.of_int 420) with
+ | Error x -> C.mdb_env_close env; Error x
+ | Ok () ->
+ let* tx = C.mdb_txn_begin' env C.null_txn Unsigned.UInt.zero in
+ let* dbi = C.mdb_dbi_open' tx Unsigned.UInt.zero in
+ let* () = C.mdb_txn_commit tx in
+ Ok {env; dbi}
+ end
+ |> Result.map_error Error.lmdb_error
+
+let start ({env; dbi} : connection) =
+ C.mdb_txn_begin' env C.null_txn Unsigned.UInt.zero
+ |> Result.map (fun tx -> {tx; dbi})
+ |> Result.map_error Error.lmdb_error
+
+let commit ({tx; _} : transaction) = C.mdb_txn_commit tx |> Result.map_error Error.lmdb_error
+let abort ({tx; _} : transaction) = C.mdb_txn_abort tx |> Result.map_error Error.lmdb_error
+
+(*
+ * FIXME: `blob_of_bytes` and `bytes_of_blob` should not exist.
+ * Rather, the FFI should know how to serialize a blob directly. This
+ * makes it so that we do not depend on the OCaml `bytes` type and
+ * opens the path for avoiding a copy on the FFI boundary later on
+ *)
+
+let get ({tx; dbi} : transaction) (h : Concepts.Hash.hash) =
+ begin match C.mdb_get' tx dbi (Concepts.Hash.bytes_of_hash h) with
+ | Ok x -> Ok (Some (Concepts.Representation.blob_of_bytes x))
+ | Error e when e = C.Errors.mdb_notfound -> Ok None
+ | Error e -> Error e
+ end
+ |> Result.map_error Error.lmdb_error
+
+let put ({tx; dbi} : transaction) (h : Concepts.Hash.hash) (b : Concepts.Representation.blob) =
+ C.mdb_put' tx dbi (Concepts.Hash.bytes_of_hash h)
+ (Concepts.Representation.bytes_of_blob b)
+ Unsigned.UInt.zero
+ |> Result.map_error Error.lmdb_error
diff --git a/lib/backend/storage/LMDB.mli b/lib/backend/storage/LMDB.mli
index dde4b6e..b4bab77 100644
--- a/lib/backend/storage/LMDB.mli
+++ b/lib/backend/storage/LMDB.mli
@@ -1 +1 @@
-include Abstract.Storage
+include Abstract.Storage.STORAGE
diff --git a/lib/concepts/Condition.ml b/lib/concepts/Condition.ml
new file mode 100644
index 0000000..cc0f7a7
--- /dev/null
+++ b/lib/concepts/Condition.ml
@@ -0,0 +1,29 @@
+type properties = (string, Value.value) BatMap.t
+type ps = properties -> properties
+
+type condition =
+ {name: string; message: string; backtrace: Printexc.raw_backtrace; properties: properties}
+
+let ( |=| ) name value = fun p -> BatMap.add name value p
+let ( & ) l r = fun p -> r (l p)
+let empty = fun p -> p
+
+let condition name message ?parent props =
+ { name;
+ message;
+ backtrace= Printexc.get_callstack 256;
+ properties=
+ parent
+ |> Option.map (fun {properties; _} -> properties)
+ |> Option.value ~default:BatMap.empty
+ |> props }
+
+let to_string_hum {name; message; properties; backtrace} =
+ let properties' =
+ properties
+ |> BatMap.to_seq
+ |> BatSeq.to_string ~first:"" ~last:"" ~sep:"\n" (fun (k, v) ->
+ "\t" ^ k ^ ": " ^ Value.to_string v )
+ in
+ let backtrace' = Printexc.raw_backtrace_to_string backtrace in
+ name ^ ": " ^ message ^ "\n" ^ properties' ^ "\n\nBacktrace:\n" ^ backtrace'
diff --git a/lib/concepts/Condition.mli b/lib/concepts/Condition.mli
new file mode 100644
index 0000000..fba43e6
--- /dev/null
+++ b/lib/concepts/Condition.mli
@@ -0,0 +1,14 @@
+(** an abstract type representing an error condition *)
+type condition
+
+type ps
+
+val ( |=| ) : string -> Value.value -> ps
+val ( & ) : ps -> ps -> ps
+val empty : ps
+
+(** construct a condition out of a name, a message, a set of properties and an optional parent *)
+val condition : string -> string -> ?parent:condition -> ps -> condition
+
+(** print a condition to a human-readable string *)
+val to_string_hum : condition -> string
diff --git a/lib/concepts/Hash.ml b/lib/concepts/Hash.ml
index 4c6cd8a..681ec4a 100644
--- a/lib/concepts/Hash.ml
+++ b/lib/concepts/Hash.ml
@@ -1,5 +1,5 @@
-type hash = ()
+type hash = bytes
-val hash_of_value (_: Value.value) = failwith "TODO"
-
-val hash_equals (_: hash) (_: hash) = failwith "TODO"
+let hash_of_value (_ : Value.value) = failwith "TODO"
+let hash_equals (_ : hash) (_ : hash) = failwith "TODO"
+let bytes_of_hash (h : hash) = h
diff --git a/lib/concepts/Hash.mli b/lib/concepts/Hash.mli
index dc25669..b100cd7 100644
--- a/lib/concepts/Hash.mli
+++ b/lib/concepts/Hash.mli
@@ -1,8 +1,11 @@
-(** An abstract value representing a hash *)
+(** an abstract value representing a hash *)
type hash
-(** Given a value, compute it's hash *)
-val hash_of_value: Value.value -> hash
+(** given a value, compute it's hash *)
+val hash_of_value : Value.value -> hash
-(** Given two hashes, are they the same? *)
-val hash_equals: hash -> hash -> bool
+(** given two hashes, are they the same? *)
+val hash_equals : hash -> hash -> bool
+
+(** given a hash, return a `bytes` representation of it *)
+val bytes_of_hash : hash -> bytes
diff --git a/lib/concepts/Path.ml b/lib/concepts/Path.ml
new file mode 100644
index 0000000..c751301
--- /dev/null
+++ b/lib/concepts/Path.ml
@@ -0,0 +1,16 @@
+type t = string BatFingerTree.t
+
+let empty : t = BatFingerTree.empty
+let of_list (components : string list) : t = BatFingerTree.of_list components
+let to_list (path : t) : string list = BatFingerTree.to_list path
+let snoc (path : t) (component : string) : t = BatFingerTree.snoc path component
+let to_string (path : t) : string = String.concat "/" (to_list path)
+
+let is_prefix ~(prefix : t) (path : t) : bool =
+ let rec walk prefix path =
+ match prefix, path with
+ | [], _ -> true
+ | p :: prefix, x :: path -> p = x && walk prefix path
+ | _ :: _, [] -> false
+ in
+ walk (to_list prefix) (to_list path)
diff --git a/lib/concepts/Path.mli b/lib/concepts/Path.mli
new file mode 100644
index 0000000..bec4221
--- /dev/null
+++ b/lib/concepts/Path.mli
@@ -0,0 +1,8 @@
+type t = string BatFingerTree.t
+
+val empty : t
+val of_list : string list -> t
+val to_list : t -> string list
+val snoc : t -> string -> t
+val to_string : t -> string
+val is_prefix : prefix:t -> t -> bool
diff --git a/lib/concepts/Representation.ml b/lib/concepts/Representation.ml
index 4df7812..d353a38 100644
--- a/lib/concepts/Representation.ml
+++ b/lib/concepts/Representation.ml
@@ -1,5 +1,6 @@
type blob = unit
-let blob_of_value (_: Value.value) = failwith "TODO"
-
-let value_of_blob (_: blob) = failwith "TODO"
+let blob_of_value (_ : Value.value) = failwith "TODO"
+let value_of_blob (_ : blob) = failwith "TODO"
+let blob_of_bytes (_ : bytes) = failwith "TODO"
+let bytes_of_blob (_ : blob) = failwith "TODO"
diff --git a/lib/concepts/Representation.mli b/lib/concepts/Representation.mli
index 9d0a8f5..21c43a4 100644
--- a/lib/concepts/Representation.mli
+++ b/lib/concepts/Representation.mli
@@ -2,6 +2,15 @@
type blob
(** encode a blob into a value *)
-val blob_of_value: Value.value -> blob
+val blob_of_value : Value.value -> blob
+
(** decode a value from a blob *)
-val value_of_blob: blob -> Value.value
+val value_of_blob : blob -> Value.value
+
+(* TODO: see the comments on Backend.Storage.LMDB *)
+
+(** create a blob from the data represented by bytes *)
+val blob_of_bytes : bytes -> blob
+
+(** return a bytes containing the data underlying a blob *)
+val bytes_of_blob : blob -> bytes
diff --git a/lib/concepts/Tuple.ml b/lib/concepts/Tuple.ml
new file mode 100644
index 0000000..3050c13
--- /dev/null
+++ b/lib/concepts/Tuple.ml
@@ -0,0 +1,46 @@
+module AttributeMap = BatMap.String
+
+(** A tuple: a finite map from attribute name to attribute. *)
+type t = Value.value AttributeMap.t
+
+(** [empty] The tuple with no attributes. *)
+let empty : t = AttributeMap.empty
+
+(** [access name t] Looks up the attribute bound to [name] in [t].
+ @param name attribute name to look up.
+ @param t tuple to search.
+ @return the attribute, or [None] when [name] is unbound. *)
+let access (name : string) (t : t) : Value.value option = AttributeMap.find_opt name t
+
+(** [merge left right] Unions two tuples. Shared attributes take the
+ [left] value; one-sided attributes are kept. Precondition: shared
+ attributes agree (the natural-join matcher guarantees this), so the
+ [left] bias is never observable.
+ @param left first tuple.
+ @param right second tuple.
+ @return the merged tuple. *)
+let merge (left : t) (right : t) : t =
+ let merger _ (left : Value.value option) (right : Value.value option) : Value.value option =
+ match left, right with (Some _ as v), _ | None, (Some _ as v) -> v | None, None -> None
+ in
+ AttributeMap.merge merger left right
+
+(** [project keep t] Keeps only the attributes of [t] whose names are
+ in [keep].
+ @param keep set of attribute names to retain.
+ @param t tuple to project.
+ @return the projected tuple. *)
+let project (keep : BatSet.String.t) (t : t) : t =
+ AttributeMap.filter (fun name _ -> BatSet.String.mem name keep) t
+
+(** [rename mapping t] Renames the attributes of [t] through
+ [mapping]; names absent from [mapping] are left unchanged.
+ @param mapping map from old name to new name.
+ @param t tuple to rename.
+ @return the renamed tuple. *)
+let rename (mapping : string BatMap.String.t) (t : t) : t =
+ AttributeMap.fold
+ (fun name attr acc ->
+ let name = BatMap.String.find_default name name mapping in
+ AttributeMap.add name attr acc )
+ t AttributeMap.empty
diff --git a/lib/concepts/Tuple.mli b/lib/concepts/Tuple.mli
new file mode 100644
index 0000000..07f7a24
--- /dev/null
+++ b/lib/concepts/Tuple.mli
@@ -0,0 +1,9 @@
+module AttributeMap = BatMap.String
+
+type t = Value.value AttributeMap.t
+
+val empty : t
+val access : string -> t -> Value.value option
+val merge : t -> t -> t
+val project : BatSet.String.t -> t -> t
+val rename : string BatMap.String.t -> t -> t
diff --git a/lib/concepts/Value.ml b/lib/concepts/Value.ml
index 3e57717..65a0439 100644
--- a/lib/concepts/Value.ml
+++ b/lib/concepts/Value.ml
@@ -1,3 +1,3 @@
-type value
- = String of string
- | Integer of int
+type value = String of string | Integer of int
+
+let to_string = function String s -> s | Integer n -> Int.to_string n
diff --git a/lib/concepts/Value.mli b/lib/concepts/Value.mli
index 6cb09ad..9644443 100644
--- a/lib/concepts/Value.mli
+++ b/lib/concepts/Value.mli
@@ -1,4 +1,5 @@
-(** A tagged concrete value *)
-type value
- = String of string
- | Integer of int
+(** a tagged concrete value *)
+type value = String of string | Integer of int
+
+(** given a value, return a string representation of it *)
+val to_string : value -> string
diff --git a/lib/dune b/lib/dune
index 7ab7df1..6fc5388 100644
--- a/lib/dune
+++ b/lib/dune
@@ -1,4 +1,6 @@
+(include_subdirs qualified)
+
(library
(name rnt)
(public_name rnt)
- (libraries batteries))
+ (libraries batteries ctypes ctypes-foreign))