···11+MIT License
22+33+Copyright (c) 2025 Thomas Gazagnaire
44+55+Permission is hereby granted, free of charge, to any person obtaining a copy
66+of this software and associated documentation files (the "Software"), to deal
77+in the Software without restriction, including without limitation the rights
88+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
99+copies of the Software, and to permit persons to whom the Software is
1010+furnished to do so, subject to the following conditions:
1111+1212+The above copyright notice and this permission notice shall be included in all
1313+copies or substantial portions of the Software.
1414+1515+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1616+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1717+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1818+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1919+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2020+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2121+SOFTWARE.
+67
README.md
···11+# sqlite
22+33+Minimal SQLite key-value store for OCaml.
44+55+## Overview
66+77+A simple key-value store backed by SQLite with support for:
88+- Namespaced tables for organizing data
99+- WAL mode for concurrent access
1010+- Efficient batch operations
1111+- Eio-compatible synchronous API
1212+1313+## Installation
1414+1515+```
1616+opam install sqlite
1717+```
1818+1919+## Usage
2020+2121+```ocaml
2222+(* Open or create a database *)
2323+let db = Sqlite.create (Eio.Path.(fs / "data.db")) in
2424+2525+(* Basic key-value operations *)
2626+Sqlite.put db "key1" "value1";
2727+let value = Sqlite.get db "key1" in (* Some "value1" *)
2828+2929+(* Namespaced tables *)
3030+let blocks = Sqlite.Table.create db ~name:"blocks" in
3131+Sqlite.Table.put blocks "cid1" "data1";
3232+3333+(* Sync to disk *)
3434+Sqlite.sync db;
3535+3636+(* Close when done *)
3737+Sqlite.close db
3838+```
3939+4040+## API
4141+4242+### Database
4343+4444+- `Sqlite.create path` - Open or create a SQLite database at path
4545+- `Sqlite.get db key` - Get value for key, or None
4646+- `Sqlite.put db key value` - Store value at key
4747+- `Sqlite.delete db key` - Remove key
4848+- `Sqlite.mem db key` - Check if key exists
4949+- `Sqlite.iter db ~f` - Iterate over all entries
5050+- `Sqlite.fold db ~init ~f` - Fold over all entries
5151+- `Sqlite.sync db` - Flush to disk (WAL checkpoint)
5252+- `Sqlite.close db` - Close the database
5353+5454+### Namespaced Tables
5555+5656+- `Sqlite.Table.create db ~name` - Create or open a named table
5757+- `Sqlite.Table.get`, `put`, `delete`, `mem`, `iter` - Same as database operations
5858+5959+## Related Work
6060+6161+- [sqlite3-ocaml](https://github.com/mmottl/sqlite3-ocaml) - Low-level SQLite3 bindings (used internally)
6262+- [ezsqlite](https://opam.ocaml.org/packages/ezsqlite/) - Alternative SQLite bindings with extensions
6363+- [irmin](https://github.com/mirage/irmin) - Git-like distributed database (different use case)
6464+6565+## License
6666+6767+MIT License. See [LICENSE.md](LICENSE.md) for details.
+23
dune-project
···11+(lang dune 3.0)
22+33+(name sqlite)
44+55+(generate_opam_files true)
66+77+(license MIT)
88+(authors "Thomas Gazagnaire")
99+(maintainers "Thomas Gazagnaire")
1010+(source (uri https://tangled.org/gazagnaire.org/ocaml-sqlite))
1111+1212+(package
1313+ (name sqlite)
1414+ (synopsis "Minimal SQLite key-value store for OCaml")
1515+ (description
1616+ "A simple key-value store backed by SQLite with support for namespaced tables, WAL mode, and efficient batch operations.")
1717+ (depends
1818+ (ocaml (>= 5.1))
1919+ (eio (>= 1.0))
2020+ (sqlite3 (>= 5.0))
2121+ (alcotest :with-test)
2222+ (eio_main :with-test)
2323+ (crowbar :with-test)))
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Thomas Gazagnaire. All rights reserved.
33+ SPDX-License-Identifier: MIT
44+ ---------------------------------------------------------------------------*)
55+66+type t = {
77+ db : Sqlite3.db;
88+ get_stmt : Sqlite3.stmt;
99+ put_stmt : Sqlite3.stmt;
1010+ delete_stmt : Sqlite3.stmt;
1111+ mem_stmt : Sqlite3.stmt;
1212+ iter_stmt : Sqlite3.stmt;
1313+}
1414+1515+let check_rc db rc =
1616+ if rc <> Sqlite3.Rc.OK && rc <> Sqlite3.Rc.DONE then
1717+ failwith (Printf.sprintf "SQLite error: %s" (Sqlite3.errmsg db))
1818+1919+let create path =
2020+ let path_str = snd (Eio.Path.split path) in
2121+ let db = Sqlite3.db_open path_str in
2222+ (* Enable WAL mode for concurrent access *)
2323+ check_rc db (Sqlite3.exec db "PRAGMA journal_mode = WAL");
2424+ check_rc db (Sqlite3.exec db "PRAGMA synchronous = NORMAL");
2525+ (* Create default KV table *)
2626+ check_rc db
2727+ (Sqlite3.exec db
2828+ "CREATE TABLE IF NOT EXISTS kv (key TEXT PRIMARY KEY, value BLOB NOT \
2929+ NULL)");
3030+ (* Prepare statements *)
3131+ let get_stmt = Sqlite3.prepare db "SELECT value FROM kv WHERE key = ?" in
3232+ let put_stmt =
3333+ Sqlite3.prepare db
3434+ "INSERT OR REPLACE INTO kv (key, value) VALUES (?, ?)"
3535+ in
3636+ let delete_stmt = Sqlite3.prepare db "DELETE FROM kv WHERE key = ?" in
3737+ let mem_stmt = Sqlite3.prepare db "SELECT 1 FROM kv WHERE key = ?" in
3838+ let iter_stmt = Sqlite3.prepare db "SELECT key, value FROM kv" in
3939+ { db; get_stmt; put_stmt; delete_stmt; mem_stmt; iter_stmt }
4040+4141+let get t key =
4242+ let stmt = t.get_stmt in
4343+ check_rc t.db (Sqlite3.reset stmt);
4444+ check_rc t.db (Sqlite3.bind_text stmt 1 key);
4545+ match Sqlite3.step stmt with
4646+ | Sqlite3.Rc.ROW -> Some (Sqlite3.column_blob stmt 0)
4747+ | Sqlite3.Rc.DONE -> None
4848+ | rc -> failwith (Printf.sprintf "SQLite step error: %s" (Sqlite3.Rc.to_string rc))
4949+5050+let put t key value =
5151+ let stmt = t.put_stmt in
5252+ check_rc t.db (Sqlite3.reset stmt);
5353+ check_rc t.db (Sqlite3.bind_text stmt 1 key);
5454+ check_rc t.db (Sqlite3.bind_blob stmt 2 value);
5555+ check_rc t.db (Sqlite3.step stmt)
5656+5757+let delete t key =
5858+ let stmt = t.delete_stmt in
5959+ check_rc t.db (Sqlite3.reset stmt);
6060+ check_rc t.db (Sqlite3.bind_text stmt 1 key);
6161+ check_rc t.db (Sqlite3.step stmt)
6262+6363+let mem t key =
6464+ let stmt = t.mem_stmt in
6565+ check_rc t.db (Sqlite3.reset stmt);
6666+ check_rc t.db (Sqlite3.bind_text stmt 1 key);
6767+ match Sqlite3.step stmt with
6868+ | Sqlite3.Rc.ROW -> true
6969+ | Sqlite3.Rc.DONE -> false
7070+ | rc -> failwith (Printf.sprintf "SQLite step error: %s" (Sqlite3.Rc.to_string rc))
7171+7272+let iter t ~f =
7373+ let stmt = t.iter_stmt in
7474+ check_rc t.db (Sqlite3.reset stmt);
7575+ let rec loop () =
7676+ match Sqlite3.step stmt with
7777+ | Sqlite3.Rc.ROW ->
7878+ let key = Sqlite3.column_text stmt 0 in
7979+ let value = Sqlite3.column_blob stmt 1 in
8080+ f key value;
8181+ loop ()
8282+ | Sqlite3.Rc.DONE -> ()
8383+ | rc -> failwith (Printf.sprintf "SQLite step error: %s" (Sqlite3.Rc.to_string rc))
8484+ in
8585+ loop ()
8686+8787+let fold t ~init ~f =
8888+ let acc = ref init in
8989+ iter t ~f:(fun k v -> acc := f k v !acc);
9090+ !acc
9191+9292+let sync t = check_rc t.db (Sqlite3.exec t.db "PRAGMA wal_checkpoint(TRUNCATE)")
9393+9494+let close t =
9595+ ignore (Sqlite3.finalize t.get_stmt);
9696+ ignore (Sqlite3.finalize t.put_stmt);
9797+ ignore (Sqlite3.finalize t.delete_stmt);
9898+ ignore (Sqlite3.finalize t.mem_stmt);
9999+ ignore (Sqlite3.finalize t.iter_stmt);
100100+ ignore (Sqlite3.db_close t.db)
101101+102102+(* Namespaced Tables *)
103103+104104+module Table = struct
105105+ type db = t
106106+107107+ type t = {
108108+ parent : db;
109109+ name : string;
110110+ get_stmt : Sqlite3.stmt;
111111+ put_stmt : Sqlite3.stmt;
112112+ delete_stmt : Sqlite3.stmt;
113113+ mem_stmt : Sqlite3.stmt;
114114+ iter_stmt : Sqlite3.stmt;
115115+ }
116116+117117+ let valid_name name =
118118+ String.length name > 0
119119+ && String.for_all
120120+ (fun c ->
121121+ (c >= 'a' && c <= 'z')
122122+ || (c >= 'A' && c <= 'Z')
123123+ || (c >= '0' && c <= '9')
124124+ || c = '_')
125125+ name
126126+127127+ let create parent ~name =
128128+ if not (valid_name name) then
129129+ invalid_arg (Printf.sprintf "Invalid table name: %S" name);
130130+ let table_name = name ^ "_kv" in
131131+ let db = parent.db in
132132+ (* Create table *)
133133+ check_rc db
134134+ (Sqlite3.exec db
135135+ (Printf.sprintf
136136+ "CREATE TABLE IF NOT EXISTS %s (key TEXT PRIMARY KEY, value BLOB \
137137+ NOT NULL)"
138138+ table_name));
139139+ (* Prepare statements *)
140140+ let get_stmt =
141141+ Sqlite3.prepare db
142142+ (Printf.sprintf "SELECT value FROM %s WHERE key = ?" table_name)
143143+ in
144144+ let put_stmt =
145145+ Sqlite3.prepare db
146146+ (Printf.sprintf "INSERT OR REPLACE INTO %s (key, value) VALUES (?, ?)"
147147+ table_name)
148148+ in
149149+ let delete_stmt =
150150+ Sqlite3.prepare db
151151+ (Printf.sprintf "DELETE FROM %s WHERE key = ?" table_name)
152152+ in
153153+ let mem_stmt =
154154+ Sqlite3.prepare db
155155+ (Printf.sprintf "SELECT 1 FROM %s WHERE key = ?" table_name)
156156+ in
157157+ let iter_stmt =
158158+ Sqlite3.prepare db
159159+ (Printf.sprintf "SELECT key, value FROM %s" table_name)
160160+ in
161161+ { parent; name; get_stmt; put_stmt; delete_stmt; mem_stmt; iter_stmt }
162162+163163+ let get t key =
164164+ let stmt = t.get_stmt in
165165+ check_rc t.parent.db (Sqlite3.reset stmt);
166166+ check_rc t.parent.db (Sqlite3.bind_text stmt 1 key);
167167+ match Sqlite3.step stmt with
168168+ | Sqlite3.Rc.ROW -> Some (Sqlite3.column_blob stmt 0)
169169+ | Sqlite3.Rc.DONE -> None
170170+ | rc -> failwith (Printf.sprintf "SQLite step error: %s" (Sqlite3.Rc.to_string rc))
171171+172172+ let put t key value =
173173+ let stmt = t.put_stmt in
174174+ check_rc t.parent.db (Sqlite3.reset stmt);
175175+ check_rc t.parent.db (Sqlite3.bind_text stmt 1 key);
176176+ check_rc t.parent.db (Sqlite3.bind_blob stmt 2 value);
177177+ check_rc t.parent.db (Sqlite3.step stmt)
178178+179179+ let delete t key =
180180+ let stmt = t.delete_stmt in
181181+ check_rc t.parent.db (Sqlite3.reset stmt);
182182+ check_rc t.parent.db (Sqlite3.bind_text stmt 1 key);
183183+ check_rc t.parent.db (Sqlite3.step stmt)
184184+185185+ let mem t key =
186186+ let stmt = t.mem_stmt in
187187+ check_rc t.parent.db (Sqlite3.reset stmt);
188188+ check_rc t.parent.db (Sqlite3.bind_text stmt 1 key);
189189+ match Sqlite3.step stmt with
190190+ | Sqlite3.Rc.ROW -> true
191191+ | Sqlite3.Rc.DONE -> false
192192+ | rc -> failwith (Printf.sprintf "SQLite step error: %s" (Sqlite3.Rc.to_string rc))
193193+194194+ let iter t ~f =
195195+ let stmt = t.iter_stmt in
196196+ check_rc t.parent.db (Sqlite3.reset stmt);
197197+ let rec loop () =
198198+ match Sqlite3.step stmt with
199199+ | Sqlite3.Rc.ROW ->
200200+ let key = Sqlite3.column_text stmt 0 in
201201+ let value = Sqlite3.column_blob stmt 1 in
202202+ f key value;
203203+ loop ()
204204+ | Sqlite3.Rc.DONE -> ()
205205+ | rc -> failwith (Printf.sprintf "SQLite step error: %s" (Sqlite3.Rc.to_string rc))
206206+ in
207207+ loop ()
208208+end
+71
lib/sqlite.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Thomas Gazagnaire. All rights reserved.
33+ SPDX-License-Identifier: MIT
44+ ---------------------------------------------------------------------------*)
55+66+(** Minimal SQLite key-value store.
77+88+ A simple key-value store backed by SQLite with support for namespaced
99+ tables, WAL mode, and efficient batch operations. *)
1010+1111+type t
1212+(** A SQLite-backed key-value store. *)
1313+1414+val create : Eio.Fs.dir_ty Eio.Path.t -> t
1515+(** [create path] opens or creates a SQLite database at [path].
1616+ Enables WAL mode for concurrent access. *)
1717+1818+val get : t -> string -> string option
1919+(** [get t key] returns the value for [key], or [None] if not found. *)
2020+2121+val put : t -> string -> string -> unit
2222+(** [put t key value] stores [value] at [key], replacing any existing value. *)
2323+2424+val delete : t -> string -> unit
2525+(** [delete t key] removes [key] from the store. No-op if key doesn't exist. *)
2626+2727+val mem : t -> string -> bool
2828+(** [mem t key] is [true] if [key] exists in the store. *)
2929+3030+val iter : t -> f:(string -> string -> unit) -> unit
3131+(** [iter t ~f] calls [f key value] for each entry in the store. *)
3232+3333+val fold : t -> init:'a -> f:(string -> string -> 'a -> 'a) -> 'a
3434+(** [fold t ~init ~f] folds over all entries in the store. *)
3535+3636+val sync : t -> unit
3737+(** [sync t] flushes to disk by performing a WAL checkpoint. *)
3838+3939+val close : t -> unit
4040+(** [close t] closes the database connection. *)
4141+4242+(** {1 Namespaced Tables}
4343+4444+ Tables provide isolated key-value namespaces within a single database. *)
4545+4646+module Table : sig
4747+ type db = t
4848+ (** The parent database type. *)
4949+5050+ type t
5151+ (** A namespaced table within a database. *)
5252+5353+ val create : db -> name:string -> t
5454+ (** [create db ~name] creates or opens a table named [name] within [db].
5555+ The table name must be a valid SQL identifier. *)
5656+5757+ val get : t -> string -> string option
5858+ (** [get t key] returns the value for [key], or [None]. *)
5959+6060+ val put : t -> string -> string -> unit
6161+ (** [put t key value] stores [value] at [key]. *)
6262+6363+ val delete : t -> string -> unit
6464+ (** [delete t key] removes [key] from the table. *)
6565+6666+ val mem : t -> string -> bool
6767+ (** [mem t key] is [true] if [key] exists in the table. *)
6868+6969+ val iter : t -> f:(string -> string -> unit) -> unit
7070+ (** [iter t ~f] calls [f key value] for each entry in the table. *)
7171+end
+82
test/cram/interop.t
···11+Test interoperability with SQLite CLI
22+33+Create a test database using our OCaml library:
44+55+ $ cat > test_create.ml << 'EOF'
66+ > let () =
77+ > Eio_main.run @@ fun env ->
88+ > let cwd = Eio.Stdenv.cwd env in
99+ > let db = Sqlite.create Eio.Path.(cwd / "test.db") in
1010+ > Sqlite.put db "key1" "value1";
1111+ > Sqlite.put db "key2" "value2";
1212+ > Sqlite.put db "binary" "\x00\x01\x02\xff";
1313+ > let table = Sqlite.Table.create db ~name:"blocks" in
1414+ > Sqlite.Table.put table "cid1" "block_data_1";
1515+ > Sqlite.Table.put table "cid2" "block_data_2";
1616+ > Sqlite.close db;
1717+ > print_endline "Database created"
1818+ > EOF
1919+2020+ $ ocamlfind ocamlopt -package sqlite,eio_main -linkpkg test_create.ml -o test_create 2>/dev/null || echo "Build requires dune"
2121+ Build requires dune
2222+2323+Skip CLI tests if sqlite3 CLI is not available:
2424+2525+ $ which sqlite3 >/dev/null 2>&1 || exit 0
2626+2727+Create test database using sqlite3 CLI directly:
2828+2929+ $ sqlite3 cli_test.db "CREATE TABLE kv (key TEXT PRIMARY KEY, value BLOB NOT NULL)"
3030+ $ sqlite3 cli_test.db "INSERT INTO kv VALUES ('hello', 'world')"
3131+ $ sqlite3 cli_test.db "INSERT INTO kv VALUES ('test', 'data')"
3232+3333+Verify data with CLI:
3434+3535+ $ sqlite3 cli_test.db "SELECT key, value FROM kv ORDER BY key"
3636+ hello|world
3737+ test|data
3838+3939+Create OCaml reader to verify CLI-created database:
4040+4141+ $ cat > test_read.ml << 'EOF'
4242+ > let () =
4343+ > Eio_main.run @@ fun env ->
4444+ > let cwd = Eio.Stdenv.cwd env in
4545+ > let db = Sqlite.create Eio.Path.(cwd / "cli_test.db") in
4646+ > (match Sqlite.get db "hello" with
4747+ > | Some v -> Printf.printf "hello = %s\n" v
4848+ > | None -> print_endline "hello not found");
4949+ > (match Sqlite.get db "test" with
5050+ > | Some v -> Printf.printf "test = %s\n" v
5151+ > | None -> print_endline "test not found");
5252+ > Sqlite.close db
5353+ > EOF
5454+5555+Test WAL mode pragma is set:
5656+5757+ $ sqlite3 cli_test.db "PRAGMA journal_mode"
5858+ wal
5959+6060+Verify table structure matches expected schema:
6161+6262+ $ sqlite3 cli_test.db ".schema kv"
6363+ CREATE TABLE kv (key TEXT PRIMARY KEY, value BLOB NOT NULL);
6464+6565+Test with namespaced tables:
6666+6767+ $ sqlite3 cli_test.db "CREATE TABLE blocks_kv (key TEXT PRIMARY KEY, value BLOB NOT NULL)"
6868+ $ sqlite3 cli_test.db "INSERT INTO blocks_kv VALUES ('cid1', X'deadbeef')"
6969+ $ sqlite3 cli_test.db "SELECT hex(value) FROM blocks_kv WHERE key = 'cid1'"
7070+ DEADBEEF
7171+7272+Verify tables exist:
7373+7474+ $ sqlite3 cli_test.db ".tables" | tr ' ' '\n' | sort | grep -v '^$'
7575+ blocks_kv
7676+ kv
7777+7878+Clean up:
7979+8080+ $ rm -f cli_test.db cli_test.db-wal cli_test.db-shm
8181+ $ rm -f test.db test.db-wal test.db-shm
8282+ $ rm -f test_create.ml test_read.ml test_create