An embedded, single-file key-value store for OCaml, inspired by BoltDB and LMDB.

feat: represent keys as bytes and values as Bigarray

+2
lib/bucket.ml
··· 6 6 7 7 let put _ _ _ = Ok () 8 8 9 + let put_bytes _t _k b = put _t _k (Types.value_of_bytes b) 10 + 9 11 let delete _ _ = Ok () 10 12 11 13 let create_bucket _ _ = Ok ()
+19 -8
lib/bucket.mli
··· 3 3 (** Bucket handle parameterized by transaction mode *) 4 4 type 'mode t 5 5 6 - (** Get a value by key *) 7 - val get : 'mode t -> string -> (string option, Error.t) result 6 + (** Get a value by key. 8 7 9 - (** Put a key-value pair (only in read-write mode) *) 10 - val put : Types.rw t -> string -> string -> (unit, Error.t) result 8 + The returned value is a zero-copy slice into the database and is only valid during 9 + the transaction lifetime. Copy with {!Types.bytes_of_value} if you need the value 10 + to outlive the transaction. *) 11 + val get : 'mode t -> Types.key -> (Types.value option, Error.t) result 12 + 13 + (** Put a key-value pair (only in read-write mode). 14 + 15 + The value will be copied into the database. *) 16 + val put : Types.rw t -> Types.key -> Types.value -> (unit, Error.t) result 17 + 18 + (** Put a key-value pair from bytes (convenience function) *) 19 + val put_bytes : Types.rw t -> Types.key -> bytes -> (unit, Error.t) result 11 20 12 21 (** Delete a key (only in read-write mode) *) 13 - val delete : Types.rw t -> string -> (unit, Error.t) result 22 + val delete : Types.rw t -> Types.key -> (unit, Error.t) result 23 + 24 + (** Create or open a nested bucket (only in read-write mode). 14 25 15 - (** Create or open a nested bucket (only in read-write mode) *) 16 - val create_bucket : Types.rw t -> string -> (Types.rw t, Error.t) result 26 + Bucket names are stored as keys. *) 27 + val create_bucket : Types.rw t -> Types.key -> (Types.rw t, Error.t) result 17 28 18 29 (** Open a nested bucket *) 19 - val bucket : 'mode t -> string -> ('mode t option, Error.t) result 30 + val bucket : 'mode t -> Types.key -> ('mode t option, Error.t) result 20 31 21 32 (** Create a cursor for iteration *) 22 33 val cursor : 'mode t -> 'mode Cursor.t
+16 -11
lib/cursor.mli
··· 3 3 (** Cursor handle parameterized by transaction mode *) 4 4 type 'mode t 5 5 6 - (** Convert cursor to a sequence of key-value pairs *) 7 - val to_seq : 'mode t -> (string * string) Seq.t 6 + (** Convert cursor to a sequence of key-value pairs. 7 + 8 + The keys and values in the sequence are zero-copy slices and 9 + are only valid during the transaction lifetime. 10 + 11 + Copy them if you need them to outlive the transaction. *) 12 + val to_seq : 'mode t -> (Types.key * Types.value) Seq.t 8 13 9 14 (** Seek to a specific key *) 10 - val seek : 'mode t -> string -> unit 15 + val seek : 'mode t -> Types.key -> unit 11 16 12 - (** Move to first key *) 13 - val first : 'mode t -> (string * string) option 17 + (** Move to first key. Returns a zero-copy view into the database. *) 18 + val first : 'mode t -> (Types.key * Types.value) option 14 19 15 - (** Move to last key *) 16 - val last : 'mode t -> (string * string) option 20 + (** Move to last key. Returns a zero-copy view into the database. *) 21 + val last : 'mode t -> (Types.key * Types.value) option 17 22 18 - (** Move to next key *) 19 - val next : 'mode t -> (string * string) option 23 + (** Move to next key. Returns a zero-copy view into the database. *) 24 + val next : 'mode t -> (Types.key * Types.value) option 20 25 21 - (** Move to previous key *) 22 - val prev : 'mode t -> (string * string) option 26 + (** Move to previous key. Returns a zero-copy view into the database. *) 27 + val prev : 'mode t -> (Types.key * Types.value) option
+30
lib/types.ml
··· 16 16 | Active 17 17 | Committed 18 18 | Rolled_back 19 + 20 + type key = bytes 21 + 22 + type value = (char, Bigarray.int8_unsigned_elt, Bigarray.c_layout) Bigarray.Array1.t 23 + 24 + let key_of_string s = Bytes.of_string s 25 + 26 + let string_of_key k = Bytes.to_string k 27 + 28 + let value_of_bytes b = 29 + let len = Bytes.length b in 30 + let arr = Bigarray.Array1.create Bigarray.char Bigarray.c_layout len in 31 + for i = 0 to len - 1 do 32 + Bigarray.Array1.set arr i (Bytes.get b i) 33 + done; 34 + arr 35 + ;; 36 + 37 + let bytes_of_value v = 38 + let len = Bigarray.Array1.dim v in 39 + let b = Bytes.create len in 40 + for i = 0 to len - 1 do 41 + Bytes.set b i (Bigarray.Array1.get v i) 42 + done; 43 + b 44 + ;; 45 + 46 + let value_of_string s = value_of_bytes (Bytes.of_string s) 47 + 48 + let string_of_value v = Bytes.to_string (bytes_of_value v)
+23 -5
lib/types.mli
··· 8 8 9 9 type page_id = int64 10 10 11 - type metadata = { 12 - version : int; 13 - page_size : int; 14 - root_bucket : page_id option; 15 - } 11 + type metadata = 12 + { version : int 13 + ; page_size : int 14 + ; root_bucket : page_id option 15 + } 16 16 17 17 type txn_state = 18 18 | Active 19 19 | Committed 20 20 | Rolled_back 21 + 22 + (** Key type - represented as bytes for efficient binary key handling *) 23 + type key = bytes 24 + 25 + (** Value type - opaque Bigarray slice for zero-copy access to mmap'd data. 26 + 27 + Values are only valid during the lifetime of the transaction that created them. 28 + If you need a value to outlive the transaction, copy it with {!bytes_of_value}. *) 29 + type value = (char, Bigarray.int8_unsigned_elt, Bigarray.c_layout) Bigarray.Array1.t 30 + 31 + val key_of_string : string -> key 32 + val string_of_key : key -> string 33 + 34 + val value_of_string : string -> value 35 + val string_of_value : value -> string 36 + 37 + val value_of_bytes : bytes -> value 38 + val bytes_of_value : value -> bytes
+5 -1
test/test_lithos.ml
··· 1 - let () = Alcotest.run "Lithos" [ "Error", Test_error.suite; "IO", Test_io.suite ] 1 + let () = 2 + Alcotest.run 3 + "Lithos" 4 + [ "Error", Test_error.suite; "IO", Test_io.suite; "Types", Test_types.suite ] 5 + ;;
+66
test/test_types.ml
··· 1 + (** Tests for Types module conversions *) 2 + 3 + let test_key_string_roundtrip () = 4 + let open Lithos.Types in 5 + let original = "hello_world" in 6 + let key = key_of_string original in 7 + let result = string_of_key key in 8 + Alcotest.(check string) "key roundtrip" original result 9 + ;; 10 + 11 + let test_value_string_roundtrip () = 12 + let open Lithos.Types in 13 + let original = "test_value_123" in 14 + let value = value_of_string original in 15 + let result = string_of_value value in 16 + Alcotest.(check string) "value string roundtrip" original result 17 + ;; 18 + 19 + let test_value_bytes_roundtrip () = 20 + let open Lithos.Types in 21 + let original = Bytes.of_string "binary_data" in 22 + let value = value_of_bytes original in 23 + let result = bytes_of_value value in 24 + Alcotest.(check bool) "value bytes roundtrip" true (Bytes.equal original result) 25 + ;; 26 + 27 + let test_empty_key () = 28 + let open Lithos.Types in 29 + let key = key_of_string "" in 30 + let result = string_of_key key in 31 + Alcotest.(check string) "empty key" "" result 32 + ;; 33 + 34 + let test_empty_value () = 35 + let open Lithos.Types in 36 + let value = value_of_string "" in 37 + let result = string_of_value value in 38 + Alcotest.(check string) "empty value" "" result 39 + ;; 40 + 41 + let test_binary_data () = 42 + let open Lithos.Types in 43 + let original = Bytes.of_string "\x00\x01\x02\xff\xfe\xfd" in 44 + let value = value_of_bytes original in 45 + let result = bytes_of_value value in 46 + Alcotest.(check bool) "binary data preservation" true (Bytes.equal original result) 47 + ;; 48 + 49 + let test_value_length () = 50 + let open Lithos.Types in 51 + let original = "test" in 52 + let value = value_of_string original in 53 + let len = Bigarray.Array1.dim value in 54 + Alcotest.(check int) "value length" (String.length original) len 55 + ;; 56 + 57 + let suite = 58 + [ "key_string_roundtrip", `Quick, test_key_string_roundtrip 59 + ; "value_string_roundtrip", `Quick, test_value_string_roundtrip 60 + ; "value_bytes_roundtrip", `Quick, test_value_bytes_roundtrip 61 + ; "empty_key", `Quick, test_empty_key 62 + ; "empty_value", `Quick, test_empty_value 63 + ; "binary_data", `Quick, test_binary_data 64 + ; "value_length", `Quick, test_value_length 65 + ] 66 + ;;