···11-let () = print_endline "Hello, World!"
11+open Cmdliner
22+33+let init_cmd =
44+ let doc = "Initialize a new lithos database" in
55+ let path_arg =
66+ let doc = "Path to the database file" in
77+ Arg.(required & pos 0 (some string) None & info [] ~docv:"PATH" ~doc)
88+ in
99+1010+ let page_size_arg =
1111+ let doc = "Page size in bytes" in
1212+ Arg.(value & opt int 4096 & info [ "page-size" ] ~docv:"SIZE" ~doc)
1313+ in
1414+1515+ let init_fn path page_size =
1616+ match Lithos.Io.create_database path page_size with
1717+ | Ok () ->
1818+ Printf.printf "Database created at %s with page size %d\n" path page_size;
1919+ 0
2020+ | Error err ->
2121+ Printf.eprintf "Error: %s\n" (Lithos.Error.to_string err);
2222+ 1
2323+ in
2424+2525+ let info = Cmd.info "init" ~doc in
2626+ Cmd.v info Term.(const init_fn $ path_arg $ page_size_arg)
2727+;;
2828+2929+let info_cmd =
3030+ let doc = "Display database information" in
3131+ let path_arg =
3232+ let doc = "Path to the database file" in
3333+ Arg.(required & pos 0 (some string) None & info [] ~docv:"PATH" ~doc)
3434+ in
3535+3636+ let info_fn path =
3737+ match Lithos.Io.read_header path with
3838+ | Ok meta ->
3939+ Printf.printf "Database: %s\n" path;
4040+ Printf.printf "Version: %d\n" meta.Lithos.Types.version;
4141+ Printf.printf "Page size: %d bytes\n" meta.Lithos.Types.page_size;
4242+ begin match meta.Lithos.Types.root_bucket with
4343+ | None -> Printf.printf "Root bucket: None\n"
4444+ | Some id -> Printf.printf "Root bucket: %Ld\n" id
4545+ end;
4646+ 0
4747+ | Error err ->
4848+ Printf.eprintf "Error: %s\n" (Lithos.Error.to_string err);
4949+ 1
5050+ in
5151+5252+ let cmd_info = Cmd.info "info" ~doc in
5353+ Cmd.v cmd_info Term.(const info_fn $ path_arg)
5454+;;
5555+5656+let default_cmd =
5757+ let doc = "Lithos - An embedded key-value database" in
5858+ let sdocs = Manpage.s_common_options in
5959+ let man =
6060+ [ `S Manpage.s_description
6161+ ; `P "Lithos is an embedded, transactional key-value database written in OCaml."
6262+ ; `S Manpage.s_commands
6363+ ; `S Manpage.s_common_options
6464+ ; `S Manpage.s_bugs
6565+ ; `P "Report bugs at https://github.com/desertthunder/lithos/issues"
6666+ ]
6767+ in
6868+6969+ let info = Cmd.info "lithos" ~version:"0.1.0" ~doc ~sdocs ~man in
7070+ Cmd.group info [ init_cmd; info_cmd ]
7171+;;
7272+7373+let () = exit (Cmd.eval' default_cmd)
+15
lib/bucket.ml
···11+(** Bucket operations for key-value storage *)
22+33+type 'mode t = unit
44+55+let get _ _ = Ok None
66+77+let put _ _ _ = Ok ()
88+99+let delete _ _ = Ok ()
1010+1111+let create_bucket _ _ = Ok ()
1212+1313+let bucket _ _ = Ok None
1414+1515+let cursor (_ : 'mode t) : 'mode Cursor.t = Obj.magic ()
+22
lib/bucket.mli
···11+(** Bucket operations for key-value storage *)
22+33+(** Bucket handle parameterized by transaction mode *)
44+type 'mode t
55+66+(** Get a value by key *)
77+val get : 'mode t -> string -> (string option, Error.t) result
88+99+(** Put a key-value pair (only in read-write mode) *)
1010+val put : Types.rw t -> string -> string -> (unit, Error.t) result
1111+1212+(** Delete a key (only in read-write mode) *)
1313+val delete : Types.rw t -> string -> (unit, Error.t) result
1414+1515+(** Create or open a nested bucket (only in read-write mode) *)
1616+val create_bucket : Types.rw t -> string -> (Types.rw t, Error.t) result
1717+1818+(** Open a nested bucket *)
1919+val bucket : 'mode t -> string -> ('mode t option, Error.t) result
2020+2121+(** Create a cursor for iteration *)
2222+val cursor : 'mode t -> 'mode Cursor.t
+15
lib/cursor.ml
···11+(** Cursor for iterating over key-value pairs *)
22+33+type 'mode t = unit
44+55+let to_seq _ = Seq.empty
66+77+let seek _ _ = ()
88+99+let first _ = None
1010+1111+let last _ = None
1212+1313+let next _ = None
1414+1515+let prev _ = None
+22
lib/cursor.mli
···11+(** Cursor for iterating over key-value pairs *)
22+33+(** Cursor handle parameterized by transaction mode *)
44+type 'mode t
55+66+(** Convert cursor to a sequence of key-value pairs *)
77+val to_seq : 'mode t -> (string * string) Seq.t
88+99+(** Seek to a specific key *)
1010+val seek : 'mode t -> string -> unit
1111+1212+(** Move to first key *)
1313+val first : 'mode t -> (string * string) option
1414+1515+(** Move to last key *)
1616+val last : 'mode t -> (string * string) option
1717+1818+(** Move to next key *)
1919+val next : 'mode t -> (string * string) option
2020+2121+(** Move to previous key *)
2222+val prev : 'mode t -> (string * string) option
+29
lib/db.ml
···11+(** Database handle and operations *)
22+33+type t =
44+ { path : string [@warning "-69"]
55+ ; meta : Types.metadata
66+ }
77+88+let open_db path =
99+ match Io.read_header path with
1010+ | Ok meta -> Ok { path; meta }
1111+ | Error (Error.IO_error _) ->
1212+ Result.bind (Io.create_database path 4096) (fun () ->
1313+ Result.map (fun meta -> { path; meta }) (Io.read_header path))
1414+ | Error err -> Error err
1515+;;
1616+1717+let close _ = Ok ()
1818+1919+let metadata db = db.meta
2020+2121+let view _ f =
2222+ let txn : Types.ro Txn.t = Obj.magic () in
2323+ f txn
2424+;;
2525+2626+let update _ f =
2727+ let txn : Types.rw Txn.t = Obj.magic () in
2828+ f txn
2929+;;
+19
lib/db.mli
···11+(** Database handle and operations *)
22+33+(** Opaque database handle *)
44+type t
55+66+(** Open or create a database file *)
77+val open_db : string -> (t, Error.t) result
88+99+(** Close the database *)
1010+val close : t -> (unit, Error.t) result
1111+1212+(** Get database metadata *)
1313+val metadata : t -> Types.metadata
1414+1515+(** Execute a read-only transaction *)
1616+val view : t -> (Types.ro Txn.t -> ('a, Error.t) result) -> ('a, Error.t) result
1717+1818+(** Execute a read-write transaction *)
1919+val update : t -> (Types.rw Txn.t -> ('a, Error.t) result) -> ('a, Error.t) result
+69
lib/io.ml
···11+(** Low-level I/O operations for database files *)
22+33+let magic = "LITHOS\x00\x00"
44+let version = 1
55+66+let create_database path page_size =
77+ try
88+ let fd = Unix.openfile path [ Unix.O_RDWR; Unix.O_CREAT; Unix.O_EXCL ] 0o644 in
99+ let buf = Bytes.create page_size in
1010+ Bytes.blit_string magic 0 buf 0 (String.length magic);
1111+1212+ Bytes.set_int32_le buf 8 (Int32.of_int version);
1313+ Bytes.set_int32_le buf 12 (Int32.of_int page_size);
1414+ Bytes.set_int64_le buf 16 Int64.zero;
1515+1616+ let written = Unix.write fd buf 0 page_size in
1717+ Unix.close fd;
1818+1919+ if written = page_size then Ok () else Error (Error.IO_error "Failed to write complete header")
2020+ with
2121+ | Unix.Unix_error (err, _, _) -> Error (Error.of_unix_error err path)
2222+;;
2323+2424+let read_header path =
2525+ try
2626+ let fd = Unix.openfile path [ Unix.O_RDONLY ] 0o644 in
2727+ let buf = Bytes.create 256 in
2828+ let read_bytes = Unix.read fd buf 0 256 in
2929+ Unix.close fd;
3030+3131+ if read_bytes < 24
3232+ then Error (Error.Invalid_database "File too small")
3333+ else (
3434+ let magic_read = Bytes.sub_string buf 0 8 in
3535+ if magic_read <> magic
3636+ then Error (Error.Invalid_database "Invalid magic number")
3737+ else (
3838+ let ver = Int32.to_int (Bytes.get_int32_le buf 8) in
3939+ let page_size = Int32.to_int (Bytes.get_int32_le buf 12) in
4040+ let root = Bytes.get_int64_le buf 16 in
4141+ let root_bucket = if root = Int64.zero then None else Some root in
4242+ Ok { Types.version = ver; page_size; root_bucket }))
4343+ with
4444+ | Unix.Unix_error (err, _, _) -> Error (Error.of_unix_error err path)
4545+;;
4646+4747+let write_header path metadata =
4848+ try
4949+ let fd = Unix.openfile path [ Unix.O_RDWR ] 0o644 in
5050+ let buf = Bytes.create 256 in
5151+5252+ Bytes.blit_string magic 0 buf 0 (String.length magic);
5353+ Bytes.set_int32_le buf 8 (Int32.of_int metadata.Types.version);
5454+ Bytes.set_int32_le buf 12 (Int32.of_int metadata.Types.page_size);
5555+5656+ let root =
5757+ match metadata.Types.root_bucket with
5858+ | None -> Int64.zero
5959+ | Some id -> id
6060+ in
6161+ Bytes.set_int64_le buf 16 root;
6262+6363+ let written = Unix.write fd buf 0 256 in
6464+ Unix.close fd;
6565+6666+ if written = 256 then Ok () else Error (Error.IO_error "Failed to write header")
6767+ with
6868+ | Unix.Unix_error (err, _, _) -> Error (Error.of_unix_error err path)
6969+;;
+10
lib/io.mli
···11+(** Low-level I/O operations for database files *)
22+33+(** Create a new database file with the specified page size *)
44+val create_database : string -> int -> (unit, Error.t) result
55+66+(** Read and parse the database header *)
77+val read_header : string -> (Types.metadata, Error.t) result
88+99+(** Write metadata to database header *)
1010+val write_header : string -> Types.metadata -> (unit, Error.t) result
+14
lib/txn.ml
···11+(** Transaction operations with phantom types for mode safety *)
22+33+type 'mode t = { mutable state : Types.txn_state }
44+55+let root (_ : 'mode t) : ('mode Bucket.t, Error.t) result = Ok (Obj.magic ())
66+77+let state txn = txn.state
88+99+let commit txn =
1010+ txn.state <- Types.Committed;
1111+ Ok ()
1212+;;
1313+1414+let rollback txn = txn.state <- Types.Rolled_back
+16
lib/txn.mli
···11+(** Transaction operations with phantom types for mode safety *)
22+33+(** Transaction handle parameterized by mode ('mode = ro | rw) *)
44+type 'mode t
55+66+(** Get the root bucket *)
77+val root : 'mode t -> ('mode Bucket.t, Error.t) result
88+99+(** Get transaction state *)
1010+val state : 'mode t -> Types.txn_state
1111+1212+(** Commit a read-write transaction *)
1313+val commit : Types.rw t -> (unit, Error.t) result
1414+1515+(** Rollback a transaction *)
1616+val rollback : 'mode t -> unit
+18
lib/types.ml
···11+(** Core types for Lithos *)
22+33+type ro
44+55+type rw
66+77+type page_id = int64
88+99+type metadata =
1010+ { version : int
1111+ ; page_size : int
1212+ ; root_bucket : page_id option
1313+ }
1414+1515+type txn_state =
1616+ | Active
1717+ | Committed
1818+ | Rolled_back
+20
lib/types.mli
···11+(** Core types for Lithos *)
22+33+(** Phantom type for read-only transactions *)
44+type ro
55+66+(** Phantom type for read-write transactions *)
77+type rw
88+99+type page_id = int64
1010+1111+type metadata = {
1212+ version : int;
1313+ page_size : int;
1414+ root_bucket : page_id option;
1515+}
1616+1717+type txn_state =
1818+ | Active
1919+ | Committed
2020+ | Rolled_back
+72
test/test_io.ml
···11+(** Tests for I/O module *)
22+33+let test_create_database () =
44+ let path = Filename.temp_file "lithos_test" ".db" in
55+ Unix.unlink path;
66+ let cleanup () =
77+ try Unix.unlink path with
88+ | _ -> ()
99+ in
1010+1111+ let open Lithos.Io in
1212+ match create_database path 4096 with
1313+ | Ok () ->
1414+ Alcotest.(check bool) "File exists" true (Sys.file_exists path);
1515+ cleanup ()
1616+ | Error err ->
1717+ cleanup ();
1818+ Alcotest.fail (Lithos.Error.to_string err)
1919+;;
2020+2121+let test_read_header () =
2222+ let path = Filename.temp_file "lithos_test" ".db" in
2323+ Unix.unlink path;
2424+ let cleanup () =
2525+ try Unix.unlink path with
2626+ | _ -> ()
2727+ in
2828+2929+ let open Lithos.Io in
3030+ match create_database path 4096 with
3131+ | Error err ->
3232+ cleanup ();
3333+ Alcotest.fail (Lithos.Error.to_string err)
3434+ | Ok () ->
3535+ (match read_header path with
3636+ | Ok meta ->
3737+ Alcotest.(check int) "Page size" 4096 meta.Lithos.Types.page_size;
3838+ Alcotest.(check int) "Version" 1 meta.Lithos.Types.version;
3939+ cleanup ()
4040+ | Error err ->
4141+ cleanup ();
4242+ Alcotest.fail (Lithos.Error.to_string err))
4343+;;
4444+4545+let test_invalid_database () =
4646+ let path = Filename.temp_file "lithos_test" ".txt" in
4747+ let cleanup () =
4848+ try Unix.unlink path with
4949+ | _ -> ()
5050+ in
5151+5252+ let oc = open_out path in
5353+ output_string oc "not a database file";
5454+ close_out oc;
5555+5656+ let open Lithos.Io in
5757+ match read_header path with
5858+ | Ok _ ->
5959+ cleanup ();
6060+ Alcotest.fail "Should have failed on invalid database"
6161+ | Error (Invalid_database _) -> cleanup ()
6262+ | Error err ->
6363+ cleanup ();
6464+ Alcotest.fail ("Wrong error: " ^ Lithos.Error.to_string err)
6565+;;
6666+6767+let suite =
6868+ [ "create_database", `Quick, test_create_database
6969+ ; "read_header", `Quick, test_read_header
7070+ ; "invalid_database", `Quick, test_invalid_database
7171+ ]
7272+;;
+1-3
test/test_lithos.ml
···11-(** Main test suite for Lithos *)
22-33-let () = Alcotest.run "Lithos" [ "Error", Test_error.suite ]
11+let () = Alcotest.run "Lithos" [ "Error", Test_error.suite; "IO", Test_io.suite ]