Minimal SQLite key-value store for OCaml
1(*---------------------------------------------------------------------------
2 Copyright (c) 2025 Thomas Gazagnaire. All rights reserved.
3 SPDX-License-Identifier: MIT
4 ---------------------------------------------------------------------------*)
5
6(** Pure OCaml B-tree backed key-value store.
7
8 A simple key-value store with SQLite-compatible semantics using a pure OCaml
9 B-tree implementation. Supports namespaced tables and reading any SQLite
10 database. *)
11
12type t
13(** A B-tree backed key-value store. *)
14
15val pp : t Fmt.t
16(** Pretty-print a database handle. *)
17
18(** {1 Record Values}
19
20 Re-exported from {!Btree.Record} so users don't need to depend on btree
21 directly. *)
22
23type value = Btree.Record.value =
24 | Vnull
25 | Vint of int64
26 | Vfloat of float
27 | Vblob of string
28 | Vtext of string (** Column values decoded from SQLite records. *)
29
30val pp_value : Format.formatter -> value -> unit
31(** Pretty-print a value. *)
32
33(** {1 Schema} *)
34
35type column = {
36 col_name : string;
37 col_affinity : string;
38 col_is_rowid_alias : bool;
39}
40(** A column definition parsed from CREATE TABLE SQL. [col_is_rowid_alias] is
41 [true] for [INTEGER PRIMARY KEY] columns, which are aliases for the rowid.
42*)
43
44type schema = { tbl_name : string; columns : column list; sql : string }
45(** Table schema with the original CREATE TABLE SQL. *)
46
47(** {1 Database Lifecycle} *)
48
49val v : sw:Eio.Switch.t -> Eio.Fs.dir_ty Eio.Path.t -> t
50(** [v ~sw path] creates a new database at [path]. If a file already exists at
51 [path], it will be truncated. The switch [sw] controls the lifetime of the
52 underlying file handle. *)
53
54val in_memory : unit -> t
55(** [in_memory ()] creates a purely in-memory database. No file I/O is
56 performed. Useful for testing. *)
57
58val open_ : sw:Eio.Switch.t -> Eio.Fs.dir_ty Eio.Path.t -> t
59(** [open_ ~sw path] opens an existing database at [path]. Works with any SQLite
60 database, not just ones created by this library. If the database contains a
61 [kv] table, the KV API functions below will work; otherwise, use the generic
62 read API.
63 @raise Failure if the file doesn't exist or is not a valid SQLite database.
64*)
65
66val sync : t -> unit
67(** [sync t] flushes all pending writes to disk. *)
68
69val close : t -> unit
70(** [close t] syncs and closes the database. *)
71
72(** {1 Key-Value API}
73
74 These functions operate on the default [kv] table. They raise [Failure] if
75 the database was opened from a file that has no [kv] table. *)
76
77val find : t -> string -> string option
78(** [find t key] returns the value for [key], or [None] if not found. *)
79
80val put : t -> string -> string -> unit
81(** [put t key value] stores [value] at [key], replacing any existing value. *)
82
83val delete : t -> string -> unit
84(** [delete t key] removes [key] from the store. No-op if key doesn't exist. *)
85
86val mem : t -> string -> bool
87(** [mem t key] is [true] if [key] exists in the store. *)
88
89val iter : t -> f:(string -> string -> unit) -> unit
90(** [iter t ~f] calls [f key value] for each entry in the store. *)
91
92val fold : t -> init:'a -> f:(string -> string -> 'a -> 'a) -> 'a
93(** [fold t ~init ~f] folds over all entries in the store. *)
94
95(** {1 Generic Read API}
96
97 Read any table in the database, regardless of schema. *)
98
99val tables : t -> schema list
100(** [tables t] returns the schema of every table in the database. *)
101
102val iter_table : t -> string -> f:(int64 -> value list -> unit) -> unit
103(** [iter_table t name ~f] calls [f rowid values] for each row in table [name].
104 For [INTEGER PRIMARY KEY] columns, [Vnull] is replaced with [Vint rowid].
105 Trailing [Vnull]s are padded if the record has fewer values than columns.
106 @raise Failure if the table doesn't exist. *)
107
108val fold_table :
109 t -> string -> init:'a -> f:(int64 -> value list -> 'a -> 'a) -> 'a
110(** [fold_table t name ~init ~f] folds over all rows in table [name]. *)
111
112val read_table : t -> string -> (int64 * value list) list
113(** [read_table t name] returns all rows from table [name] as a list. *)
114
115(** {1 Schema Parsing} *)
116
117val parse_create_table : string -> column list
118(** [parse_create_table sql] parses a CREATE TABLE statement and returns the
119 column definitions. Returns an empty list if parsing fails. *)
120
121(** {1 Namespaced Tables}
122
123 Tables provide isolated key-value namespaces within a single database. *)
124
125module Table : sig
126 type db = t
127 (** The parent database type. *)
128
129 type t
130 (** A namespaced table within a database. *)
131
132 val create : db -> name:string -> t
133 (** [create db ~name] creates or opens a table named [name] within [db]. The
134 table name must be a valid SQL identifier. *)
135
136 val find : t -> string -> string option
137 (** [find t key] returns the value for [key], or [None]. *)
138
139 val put : t -> string -> string -> unit
140 (** [put t key value] stores [value] at [key]. *)
141
142 val delete : t -> string -> unit
143 (** [delete t key] removes [key] from the table. *)
144
145 val mem : t -> string -> bool
146 (** [mem t key] is [true] if [key] exists in the table. *)
147
148 val iter : t -> f:(string -> string -> unit) -> unit
149 (** [iter t ~f] calls [f key value] for each entry in the table. *)
150end