Pure OCaml B-tree implementation for persistent storage
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge commit '837a470995369419f7d48edfe1135f38ae5a1e2d' as 'ocaml-btree'

+1295
+17
.gitignore
··· 1 + # OCaml build artifacts 2 + _build/ 3 + *.install 4 + *.merlin 5 + 6 + # Dune package management 7 + dune.lock/ 8 + 9 + # Editor and OS files 10 + .DS_Store 11 + *.swp 12 + *~ 13 + .vscode/ 14 + .idea/ 15 + 16 + # Opam local switch 17 + _opam/
+1
.ocamlformat
··· 1 + version=0.27.0
+21
LICENSE.md
··· 1 + MIT License 2 + 3 + Copyright (c) 2025 Thomas Gazagnaire 4 + 5 + Permission is hereby granted, free of charge, to any person obtaining a copy 6 + of this software and associated documentation files (the "Software"), to deal 7 + in the Software without restriction, including without limitation the rights 8 + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + copies of the Software, and to permit persons to whom the Software is 10 + furnished to do so, subject to the following conditions: 11 + 12 + The above copyright notice and this permission notice shall be included in all 13 + copies or substantial portions of the Software. 14 + 15 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 + SOFTWARE.
+105
README.md
··· 1 + # btree 2 + 3 + Pure OCaml B-tree implementation for persistent storage. 4 + 5 + ## Overview 6 + 7 + A B-tree implementation supporting SQLite-compatible page-based storage: 8 + 9 + - **Table B-trees**: 64-bit integer keys with data in leaves 10 + - **Index B-trees**: Arbitrary keys, no data (for secondary indexes) 11 + - **Page-based**: Interior and leaf pages with configurable size 12 + - **Overflow support**: Large records span multiple pages 13 + 14 + ## Installation 15 + 16 + ``` 17 + opam install btree 18 + ``` 19 + 20 + ## Usage 21 + 22 + ```ocaml 23 + (* Create a pager backed by a file *) 24 + let pager = Btree.Pager.create ~page_size:4096 file in 25 + 26 + (* Create a table B-tree *) 27 + let tree = Btree.Table.create pager in 28 + 29 + (* Insert records *) 30 + Btree.Table.insert tree ~rowid:1L "Hello"; 31 + Btree.Table.insert tree ~rowid:2L "World"; 32 + 33 + (* Lookup *) 34 + let data = Btree.Table.find tree 1L in (* Some "Hello" *) 35 + 36 + (* Iterate *) 37 + Btree.Table.iter tree (fun rowid data -> 38 + Printf.printf "%Ld: %s\n" rowid data) 39 + ``` 40 + 41 + ## API 42 + 43 + ### Pager 44 + 45 + The pager manages page I/O and caching: 46 + 47 + - `Pager.create ~page_size file` - Create pager with given page size 48 + - `Pager.read pager page_num` - Read a page 49 + - `Pager.write pager page_num data` - Write a page 50 + - `Pager.allocate pager` - Allocate a new page 51 + - `Pager.free pager page_num` - Free a page 52 + - `Pager.sync pager` - Sync to disk 53 + 54 + ### Table B-tree 55 + 56 + For rowid-keyed tables (like SQLite tables): 57 + 58 + - `Table.create pager` - Create a new table B-tree 59 + - `Table.open_ pager root_page` - Open existing table 60 + - `Table.insert tree ~rowid data` - Insert a record 61 + - `Table.find tree rowid` - Find by rowid 62 + - `Table.delete tree rowid` - Delete by rowid 63 + - `Table.iter tree f` - Iterate all records 64 + 65 + ### Index B-tree 66 + 67 + For arbitrary keys (like SQLite indexes): 68 + 69 + - `Index.create pager` - Create a new index B-tree 70 + - `Index.insert tree key` - Insert a key 71 + - `Index.mem tree key` - Check if key exists 72 + - `Index.delete tree key` - Delete a key 73 + - `Index.iter tree f` - Iterate all keys 74 + 75 + ## Page Format 76 + 77 + Following SQLite's B-tree page format: 78 + 79 + ### Page Header 80 + 81 + | Offset | Size | Description | 82 + |--------|------|-------------| 83 + | 0 | 1 | Page type (0x02, 0x05, 0x0a, 0x0d) | 84 + | 1 | 2 | First freeblock offset | 85 + | 3 | 2 | Number of cells | 86 + | 5 | 2 | Cell content area start | 87 + | 7 | 1 | Fragmented free bytes | 88 + | 8 | 4 | Right-most child (interior only) | 89 + 90 + ### Page Types 91 + 92 + - `0x02` - Interior index page 93 + - `0x05` - Interior table page 94 + - `0x0a` - Leaf index page 95 + - `0x0d` - Leaf table page 96 + 97 + ## Related Work 98 + 99 + - [SQLite](https://www.sqlite.org/fileformat.html) - Reference B-tree implementation 100 + - [Limbo](https://github.com/tursodatabase/limbo) - Rust SQLite implementation 101 + - [ocaml-btree (ctk21)](https://github.com/ctk21/ocaml-btree) - In-memory B-tree 102 + 103 + ## License 104 + 105 + MIT License. See [LICENSE.md](LICENSE.md) for details.
+22
dune-project
··· 1 + (lang dune 3.0) 2 + 3 + (name btree) 4 + 5 + (generate_opam_files true) 6 + 7 + (license MIT) 8 + (authors "Thomas Gazagnaire") 9 + (maintainers "Thomas Gazagnaire") 10 + (source (uri https://tangled.org/gazagnaire.org/ocaml-btree)) 11 + 12 + (package 13 + (name btree) 14 + (synopsis "Pure OCaml B-tree implementation for persistent storage") 15 + (description 16 + "A B-tree implementation supporting both table B-trees (integer keys with data) and index B-trees (arbitrary keys). Designed for SQLite-compatible persistent storage with page-based I/O.") 17 + (depends 18 + (ocaml (>= 5.1)) 19 + (eio (>= 1.0)) 20 + cstruct 21 + (alcotest :with-test) 22 + (eio_main :with-test)))
+682
lib/btree.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Thomas Gazagnaire. All rights reserved. 3 + SPDX-License-Identifier: MIT 4 + ---------------------------------------------------------------------------*) 5 + 6 + (* Varint encoding - SQLite style *) 7 + module Varint = struct 8 + let decode buf off = 9 + let rec loop acc shift i = 10 + if i >= String.length buf then (acc, i - off) 11 + else 12 + let byte = Char.code buf.[i] in 13 + let value = Int64.of_int (byte land 0x7f) in 14 + let acc = Int64.logor acc (Int64.shift_left value shift) in 15 + if byte land 0x80 = 0 then (acc, i - off + 1) 16 + else if shift >= 56 then 17 + (* 9th byte - use all 8 bits *) 18 + let byte9 = Char.code buf.[i + 1] in 19 + let acc = Int64.logor acc (Int64.shift_left (Int64.of_int byte9) 56) in 20 + (acc, i - off + 2) 21 + else loop acc (shift + 7) (i + 1) 22 + in 23 + loop 0L 0 off 24 + 25 + let size n = 26 + if n < 0L then 9 27 + else if n < 128L then 1 28 + else if n < 16384L then 2 29 + else if n < 2097152L then 3 30 + else if n < 268435456L then 4 31 + else if n < 34359738368L then 5 32 + else if n < 4398046511104L then 6 33 + else if n < 562949953421312L then 7 34 + else if n < 72057594037927936L then 8 35 + else 9 36 + 37 + let encode n = 38 + let sz = size n in 39 + let buf = Bytes.create sz in 40 + let rec loop n i = 41 + if i = sz - 1 then Bytes.set_uint8 buf i (Int64.to_int n land 0x7f) 42 + else begin 43 + Bytes.set_uint8 buf i (Int64.to_int n land 0x7f lor 0x80); 44 + loop (Int64.shift_right_logical n 7) (i + 1) 45 + end 46 + in 47 + loop n 0; 48 + Bytes.unsafe_to_string buf 49 + end 50 + 51 + (* Page types *) 52 + type page_type = 53 + | Interior_index 54 + | Interior_table 55 + | Leaf_index 56 + | Leaf_table 57 + 58 + let pp_page_type ppf = function 59 + | Interior_index -> Format.pp_print_string ppf "interior_index" 60 + | Interior_table -> Format.pp_print_string ppf "interior_table" 61 + | Leaf_index -> Format.pp_print_string ppf "leaf_index" 62 + | Leaf_table -> Format.pp_print_string ppf "leaf_table" 63 + 64 + let page_type_of_byte = function 65 + | 0x02 -> Interior_index 66 + | 0x05 -> Interior_table 67 + | 0x0a -> Leaf_index 68 + | 0x0d -> Leaf_table 69 + | b -> failwith (Printf.sprintf "Invalid page type: 0x%02x" b) 70 + 71 + let byte_of_page_type = function 72 + | Interior_index -> 0x02 73 + | Interior_table -> 0x05 74 + | Leaf_index -> 0x0a 75 + | Leaf_table -> 0x0d 76 + 77 + let page_header_size = function 78 + | Interior_index | Interior_table -> 12 79 + | Leaf_index | Leaf_table -> 8 80 + 81 + let is_interior = function 82 + | Interior_index | Interior_table -> true 83 + | Leaf_index | Leaf_table -> false 84 + 85 + (* Page header *) 86 + type page_header = { 87 + page_type : page_type; 88 + first_freeblock : int; 89 + cell_count : int; 90 + cell_content_start : int; 91 + fragmented_bytes : int; 92 + right_child : int option; 93 + } 94 + 95 + let get_u16_be buf off = 96 + (Char.code buf.[off] lsl 8) lor Char.code buf.[off + 1] 97 + 98 + let get_u32_be buf off = 99 + (Char.code buf.[off] lsl 24) 100 + lor (Char.code buf.[off + 1] lsl 16) 101 + lor (Char.code buf.[off + 2] lsl 8) 102 + lor Char.code buf.[off + 3] 103 + 104 + let set_u16_be buf off v = 105 + Bytes.set_uint8 buf off (v lsr 8); 106 + Bytes.set_uint8 buf (off + 1) (v land 0xff) 107 + 108 + let set_u32_be buf off v = 109 + Bytes.set_uint8 buf off (v lsr 24); 110 + Bytes.set_uint8 buf (off + 1) ((v lsr 16) land 0xff); 111 + Bytes.set_uint8 buf (off + 2) ((v lsr 8) land 0xff); 112 + Bytes.set_uint8 buf (off + 3) (v land 0xff) 113 + 114 + let parse_page_header buf off = 115 + let page_type = page_type_of_byte (Char.code buf.[off]) in 116 + let first_freeblock = get_u16_be buf (off + 1) in 117 + let cell_count = get_u16_be buf (off + 3) in 118 + let cell_content_start = 119 + let v = get_u16_be buf (off + 5) in 120 + if v = 0 then 65536 else v 121 + in 122 + let fragmented_bytes = Char.code buf.[off + 7] in 123 + let right_child = 124 + if is_interior page_type then Some (get_u32_be buf (off + 8)) else None 125 + in 126 + { 127 + page_type; 128 + first_freeblock; 129 + cell_count; 130 + cell_content_start; 131 + fragmented_bytes; 132 + right_child; 133 + } 134 + 135 + (* Cells *) 136 + module Cell = struct 137 + type table_leaf = { 138 + rowid : int64; 139 + payload : string; 140 + overflow_page : int option; 141 + } 142 + 143 + type table_interior = { left_child : int; rowid : int64 } 144 + 145 + type index_leaf = { payload : string; overflow_page : int option } 146 + 147 + type index_interior = { 148 + left_child : int; 149 + payload : string; 150 + overflow_page : int option; 151 + } 152 + 153 + (* Calculate max payload on page - simplified *) 154 + let max_local ~usable_size ~is_table = 155 + if is_table then usable_size - 35 else ((usable_size - 12) * 64 / 255) - 23 156 + 157 + let min_local ~usable_size = 158 + ((usable_size - 12) * 32 / 255) - 23 159 + 160 + let parse_table_leaf buf off ~usable_size = 161 + let payload_size, consumed1 = Varint.decode buf off in 162 + let rowid, consumed2 = Varint.decode buf (off + consumed1) in 163 + let header_len = consumed1 + consumed2 in 164 + let payload_size = Int64.to_int payload_size in 165 + let max_local = max_local ~usable_size ~is_table:true in 166 + let min_local = min_local ~usable_size in 167 + let local_size, overflow_page = 168 + if payload_size <= max_local then (payload_size, None) 169 + else 170 + let k = min_local + ((payload_size - min_local) mod (usable_size - 4)) in 171 + let local = if k <= max_local then k else min_local in 172 + let overflow = get_u32_be buf (off + header_len + local) in 173 + (local, Some overflow) 174 + in 175 + let payload = String.sub buf (off + header_len) local_size in 176 + let total_consumed = 177 + header_len + local_size + (if overflow_page = None then 0 else 4) 178 + in 179 + ({ rowid; payload; overflow_page }, total_consumed) 180 + 181 + let parse_table_interior buf off = 182 + let left_child = get_u32_be buf off in 183 + let rowid, consumed = Varint.decode buf (off + 4) in 184 + ({ left_child; rowid }, 4 + consumed) 185 + 186 + let parse_index_leaf buf off ~usable_size = 187 + let payload_size, consumed = Varint.decode buf off in 188 + let payload_size = Int64.to_int payload_size in 189 + let max_local = max_local ~usable_size ~is_table:false in 190 + let min_local = min_local ~usable_size in 191 + let local_size, overflow_page = 192 + if payload_size <= max_local then (payload_size, None) 193 + else 194 + let k = min_local + ((payload_size - min_local) mod (usable_size - 4)) in 195 + let local = if k <= max_local then k else min_local in 196 + let overflow = get_u32_be buf (off + consumed + local) in 197 + (local, Some overflow) 198 + in 199 + let payload = String.sub buf (off + consumed) local_size in 200 + let total = consumed + local_size + (if overflow_page = None then 0 else 4) in 201 + ({ payload; overflow_page }, total) 202 + 203 + let parse_index_interior buf off ~usable_size = 204 + let left_child = get_u32_be buf off in 205 + let payload_size, consumed = Varint.decode buf (off + 4) in 206 + let payload_size = Int64.to_int payload_size in 207 + let max_local = max_local ~usable_size ~is_table:false in 208 + let min_local = min_local ~usable_size in 209 + let local_size, overflow_page = 210 + if payload_size <= max_local then (payload_size, None) 211 + else 212 + let k = min_local + ((payload_size - min_local) mod (usable_size - 4)) in 213 + let local = if k <= max_local then k else min_local in 214 + let overflow = get_u32_be buf (off + 4 + consumed + local) in 215 + (local, Some overflow) 216 + in 217 + let payload = String.sub buf (off + 4 + consumed) local_size in 218 + let total = 219 + 4 + consumed + local_size + (if overflow_page = None then 0 else 4) 220 + in 221 + ({ left_child; payload; overflow_page }, total) 222 + end 223 + 224 + (* Record format *) 225 + module Record = struct 226 + type serial_type = 227 + | Null 228 + | Int8 229 + | Int16 230 + | Int24 231 + | Int32 232 + | Int48 233 + | Int64 234 + | Float64 235 + | Zero 236 + | One 237 + | Blob of int 238 + | Text of int 239 + 240 + type value = Vnull | Vint of int64 | Vfloat of float | Vblob of string | Vtext of string 241 + 242 + let serial_type_of_int = function 243 + | 0 -> Null 244 + | 1 -> Int8 245 + | 2 -> Int16 246 + | 3 -> Int24 247 + | 4 -> Int32 248 + | 5 -> Int48 249 + | 6 -> Int64 250 + | 7 -> Float64 251 + | 8 -> Zero 252 + | 9 -> One 253 + | n when n >= 12 && n mod 2 = 0 -> Blob ((n - 12) / 2) 254 + | n when n >= 13 -> Text ((n - 13) / 2) 255 + | n -> failwith (Printf.sprintf "Invalid serial type: %d" n) 256 + 257 + let serial_type_size = function 258 + | Null | Zero | One -> 0 259 + | Int8 -> 1 260 + | Int16 -> 2 261 + | Int24 -> 3 262 + | Int32 -> 4 263 + | Int48 -> 6 264 + | Int64 | Float64 -> 8 265 + | Blob n | Text n -> n 266 + 267 + let decode_int buf off len = 268 + let rec loop acc i = 269 + if i >= len then acc 270 + else 271 + let b = Char.code buf.[off + i] in 272 + let acc = Int64.logor (Int64.shift_left acc 8) (Int64.of_int b) in 273 + loop acc (i + 1) 274 + in 275 + (* Sign extend for negative values *) 276 + let v = loop 0L 0 in 277 + if len > 0 && Char.code buf.[off] land 0x80 <> 0 then 278 + let mask = Int64.shift_left (-1L) (len * 8) in 279 + Int64.logor v mask 280 + else v 281 + 282 + let decode payload = 283 + let header_size, consumed = Varint.decode payload 0 in 284 + let header_size = Int64.to_int header_size in 285 + (* Parse serial types *) 286 + let rec parse_types off acc = 287 + if off >= header_size then List.rev acc 288 + else 289 + let st, consumed = Varint.decode payload off in 290 + let st = serial_type_of_int (Int64.to_int st) in 291 + parse_types (off + consumed) (st :: acc) 292 + in 293 + let types = parse_types consumed [] in 294 + (* Parse values *) 295 + let rec parse_values types off acc = 296 + match types with 297 + | [] -> List.rev acc 298 + | st :: rest -> 299 + let value, sz = 300 + match st with 301 + | Null -> (Vnull, 0) 302 + | Zero -> (Vint 0L, 0) 303 + | One -> (Vint 1L, 0) 304 + | Int8 -> (Vint (decode_int payload off 1), 1) 305 + | Int16 -> (Vint (decode_int payload off 2), 2) 306 + | Int24 -> (Vint (decode_int payload off 3), 3) 307 + | Int32 -> (Vint (decode_int payload off 4), 4) 308 + | Int48 -> (Vint (decode_int payload off 6), 6) 309 + | Int64 -> (Vint (decode_int payload off 8), 8) 310 + | Float64 -> 311 + let bits = decode_int payload off 8 in 312 + (Vfloat (Int64.float_of_bits bits), 8) 313 + | Blob n -> (Vblob (String.sub payload off n), n) 314 + | Text n -> (Vtext (String.sub payload off n), n) 315 + in 316 + parse_values rest (off + sz) (value :: acc) 317 + in 318 + parse_values types header_size [] 319 + 320 + let serial_type_of_value = function 321 + | Vnull -> (0, 0) 322 + | Vint 0L -> (8, 0) 323 + | Vint 1L -> (9, 0) 324 + | Vint n -> 325 + if n >= -128L && n <= 127L then (1, 1) 326 + else if n >= -32768L && n <= 32767L then (2, 2) 327 + else if n >= -8388608L && n <= 8388607L then (3, 3) 328 + else if n >= -2147483648L && n <= 2147483647L then (4, 4) 329 + else if n >= -140737488355328L && n <= 140737488355327L then (5, 6) 330 + else (6, 8) 331 + | Vfloat _ -> (7, 8) 332 + | Vblob s -> (12 + String.length s * 2, String.length s) 333 + | Vtext s -> (13 + String.length s * 2, String.length s) 334 + 335 + let encode_int buf off n len = 336 + for i = 0 to len - 1 do 337 + let shift = (len - 1 - i) * 8 in 338 + Bytes.set_uint8 buf (off + i) 339 + (Int64.to_int (Int64.shift_right n shift) land 0xff) 340 + done 341 + 342 + let encode values = 343 + (* Calculate header *) 344 + let types_and_sizes = List.map serial_type_of_value values in 345 + let header_types = 346 + List.map (fun (st, _) -> Varint.encode (Int64.of_int st)) types_and_sizes 347 + in 348 + let header_body = String.concat "" header_types in 349 + let header_size = 1 + String.length header_body in 350 + (* header size varint + types *) 351 + let body_size = List.fold_left (fun acc (_, sz) -> acc + sz) 0 types_and_sizes in 352 + let total = header_size + body_size in 353 + let buf = Bytes.create total in 354 + (* Write header size *) 355 + Bytes.set_uint8 buf 0 header_size; 356 + (* Write serial types *) 357 + let _ = 358 + List.fold_left 359 + (fun off s -> 360 + Bytes.blit_string s 0 buf off (String.length s); 361 + off + String.length s) 362 + 1 header_types 363 + in 364 + (* Write values *) 365 + let _ = 366 + List.fold_left2 367 + (fun off value (_, sz) -> 368 + (match value with 369 + | Vnull | Vint 0L | Vint 1L -> () 370 + | Vint n -> encode_int buf off n sz 371 + | Vfloat f -> encode_int buf off (Int64.bits_of_float f) 8 372 + | Vblob s | Vtext s -> Bytes.blit_string s 0 buf off sz); 373 + off + sz) 374 + header_size values types_and_sizes 375 + in 376 + Bytes.unsafe_to_string buf 377 + 378 + let pp_value ppf = function 379 + | Vnull -> Format.pp_print_string ppf "NULL" 380 + | Vint n -> Format.fprintf ppf "%Ld" n 381 + | Vfloat f -> Format.fprintf ppf "%f" f 382 + | Vblob s -> Format.fprintf ppf "BLOB(%d)" (String.length s) 383 + | Vtext s -> Format.fprintf ppf "%S" s 384 + end 385 + 386 + (* Pager *) 387 + module Pager = struct 388 + type t = { 389 + file : Eio.File.rw_ty Eio.Resource.t; 390 + page_size : int; 391 + mutable page_count : int; 392 + cache : (int, string) Hashtbl.t; 393 + dirty : (int, string) Hashtbl.t; 394 + } 395 + 396 + let create ~page_size file = 397 + let stat = Eio.File.stat file in 398 + let file_size = Optint.Int63.to_int stat.size in 399 + let page_count = if file_size = 0 then 0 else file_size / page_size in 400 + { 401 + file; 402 + page_size; 403 + page_count; 404 + cache = Hashtbl.create 64; 405 + dirty = Hashtbl.create 16; 406 + } 407 + 408 + let page_size t = t.page_size 409 + let page_count t = t.page_count 410 + 411 + let read t page_num = 412 + if page_num < 1 || page_num > t.page_count then 413 + failwith (Printf.sprintf "Invalid page number: %d" page_num); 414 + match Hashtbl.find_opt t.dirty page_num with 415 + | Some data -> data 416 + | None -> ( 417 + match Hashtbl.find_opt t.cache page_num with 418 + | Some data -> data 419 + | None -> 420 + let offset = Optint.Int63.of_int ((page_num - 1) * t.page_size) in 421 + let buf = Cstruct.create t.page_size in 422 + Eio.File.pread_exact t.file ~file_offset:offset [ buf ]; 423 + let data = Cstruct.to_string buf in 424 + Hashtbl.replace t.cache page_num data; 425 + data) 426 + 427 + let write t page_num data = 428 + if String.length data <> t.page_size then 429 + failwith "Invalid page size"; 430 + Hashtbl.replace t.dirty page_num data; 431 + Hashtbl.replace t.cache page_num data 432 + 433 + let allocate t = 434 + t.page_count <- t.page_count + 1; 435 + let data = String.make t.page_size '\x00' in 436 + Hashtbl.replace t.dirty t.page_count data; 437 + Hashtbl.replace t.cache t.page_count data; 438 + t.page_count 439 + 440 + let sync t = 441 + Hashtbl.iter 442 + (fun page_num data -> 443 + let offset = Optint.Int63.of_int ((page_num - 1) * t.page_size) in 444 + let buf = Cstruct.of_string data in 445 + Eio.File.pwrite_all t.file ~file_offset:offset [ buf ]) 446 + t.dirty; 447 + Hashtbl.clear t.dirty 448 + end 449 + 450 + (* Table B-tree *) 451 + module Table = struct 452 + type t = { pager : Pager.t; mutable root_page : int } 453 + 454 + let create pager = 455 + let root = Pager.allocate pager in 456 + (* Initialize as empty leaf page *) 457 + let page_size = Pager.page_size pager in 458 + let buf = Bytes.create page_size in 459 + Bytes.set_uint8 buf 0 (byte_of_page_type Leaf_table); 460 + set_u16_be buf 1 0; 461 + (* first freeblock *) 462 + set_u16_be buf 3 0; 463 + (* cell count *) 464 + set_u16_be buf 5 page_size; 465 + (* cell content start *) 466 + Bytes.set_uint8 buf 7 0; 467 + (* fragmented bytes *) 468 + Pager.write pager root (Bytes.unsafe_to_string buf); 469 + { pager; root_page = root } 470 + 471 + let open_ pager ~root_page = { pager; root_page } 472 + let root_page t = t.root_page 473 + 474 + let usable_size t = Pager.page_size t.pager 475 + 476 + (* Find cell pointers in a page *) 477 + let cell_pointers page header_offset header = 478 + let ptrs = Array.make header.cell_count 0 in 479 + let ptr_start = header_offset + page_header_size header.page_type in 480 + for i = 0 to header.cell_count - 1 do 481 + ptrs.(i) <- get_u16_be page (ptr_start + i * 2) 482 + done; 483 + ptrs 484 + 485 + (* Binary search for rowid in leaf page *) 486 + let search_leaf t page header rowid = 487 + let ptrs = cell_pointers page 0 header in 488 + let usable = usable_size t in 489 + let rec loop lo hi = 490 + if lo > hi then None 491 + else 492 + let mid = (lo + hi) / 2 in 493 + let cell, _ = Cell.parse_table_leaf page ptrs.(mid) ~usable_size:usable in 494 + if cell.rowid = rowid then Some cell.payload 495 + else if cell.rowid < rowid then loop (mid + 1) hi 496 + else loop lo (mid - 1) 497 + in 498 + loop 0 (header.cell_count - 1) 499 + 500 + (* Find child page for rowid in interior page *) 501 + let find_child t page header rowid = 502 + let ptrs = cell_pointers page 0 header in 503 + let rec loop i = 504 + if i >= header.cell_count then Option.get header.right_child 505 + else 506 + let cell, _ = Cell.parse_table_interior page ptrs.(i) in 507 + if rowid <= cell.rowid then cell.left_child else loop (i + 1) 508 + in 509 + loop 0 510 + 511 + let rec find_in_page t page_num rowid = 512 + let page = Pager.read t.pager page_num in 513 + let header = parse_page_header page 0 in 514 + match header.page_type with 515 + | Leaf_table -> search_leaf t page header rowid 516 + | Interior_table -> 517 + let child = find_child t page header rowid in 518 + find_in_page t child rowid 519 + | _ -> failwith "Invalid page type in table B-tree" 520 + 521 + let find t rowid = find_in_page t t.root_page rowid 522 + 523 + (* Insert - simplified, doesn't handle page splits *) 524 + let insert t ~rowid data = 525 + let page = Pager.read t.pager t.root_page in 526 + let header = parse_page_header page 0 in 527 + match header.page_type with 528 + | Leaf_table -> 529 + let page_size = Pager.page_size t.pager in 530 + let buf = Bytes.of_string page in 531 + (* Create cell *) 532 + let rowid_varint = Varint.encode rowid in 533 + let payload_size_varint = Varint.encode (Int64.of_int (String.length data)) in 534 + let cell_size = 535 + String.length payload_size_varint + String.length rowid_varint + String.length data 536 + in 537 + (* Find insertion point *) 538 + let ptrs = cell_pointers page 0 header in 539 + let usable = usable_size t in 540 + let insert_idx = 541 + let rec find i = 542 + if i >= header.cell_count then i 543 + else 544 + let cell, _ = Cell.parse_table_leaf page ptrs.(i) ~usable_size:usable in 545 + if rowid < cell.rowid then i else find (i + 1) 546 + in 547 + find 0 548 + in 549 + (* Write cell at end of cell content area *) 550 + let cell_start = header.cell_content_start - cell_size in 551 + Bytes.blit_string payload_size_varint 0 buf cell_start 552 + (String.length payload_size_varint); 553 + let off = cell_start + String.length payload_size_varint in 554 + Bytes.blit_string rowid_varint 0 buf off (String.length rowid_varint); 555 + let off = off + String.length rowid_varint in 556 + Bytes.blit_string data 0 buf off (String.length data); 557 + (* Update cell content start *) 558 + set_u16_be buf 5 cell_start; 559 + (* Insert cell pointer *) 560 + let ptr_area_start = page_header_size Leaf_table in 561 + (* Shift existing pointers *) 562 + for i = header.cell_count - 1 downto insert_idx do 563 + set_u16_be buf (ptr_area_start + (i + 1) * 2) (get_u16_be page (ptr_area_start + i * 2)) 564 + done; 565 + set_u16_be buf (ptr_area_start + insert_idx * 2) cell_start; 566 + (* Update cell count *) 567 + set_u16_be buf 3 (header.cell_count + 1); 568 + Pager.write t.pager t.root_page (Bytes.unsafe_to_string buf) 569 + | _ -> failwith "Insert into interior page not yet implemented" 570 + 571 + let delete _t _rowid = failwith "Delete not yet implemented" 572 + 573 + let iter t f = 574 + let rec iter_page page_num = 575 + let page = Pager.read t.pager page_num in 576 + let header = parse_page_header page 0 in 577 + let ptrs = cell_pointers page 0 header in 578 + let usable = usable_size t in 579 + match header.page_type with 580 + | Leaf_table -> 581 + for i = 0 to header.cell_count - 1 do 582 + let cell, _ = Cell.parse_table_leaf page ptrs.(i) ~usable_size:usable in 583 + f cell.rowid cell.payload 584 + done 585 + | Interior_table -> 586 + for i = 0 to header.cell_count - 1 do 587 + let cell, _ = Cell.parse_table_interior page ptrs.(i) in 588 + iter_page cell.left_child 589 + done; 590 + Option.iter iter_page header.right_child 591 + | _ -> failwith "Invalid page type" 592 + in 593 + iter_page t.root_page 594 + 595 + let fold t ~init ~f = 596 + let acc = ref init in 597 + iter t (fun rowid data -> acc := f rowid data !acc); 598 + !acc 599 + end 600 + 601 + (* Index B-tree *) 602 + module Index = struct 603 + type t = { pager : Pager.t; mutable root_page : int } 604 + 605 + let create pager = 606 + let root = Pager.allocate pager in 607 + let page_size = Pager.page_size pager in 608 + let buf = Bytes.create page_size in 609 + Bytes.set_uint8 buf 0 (byte_of_page_type Leaf_index); 610 + set_u16_be buf 1 0; 611 + set_u16_be buf 3 0; 612 + set_u16_be buf 5 page_size; 613 + Bytes.set_uint8 buf 7 0; 614 + Pager.write pager root (Bytes.unsafe_to_string buf); 615 + { pager; root_page = root } 616 + 617 + let open_ pager ~root_page = { pager; root_page } 618 + let root_page t = t.root_page 619 + 620 + let usable_size t = Pager.page_size t.pager 621 + 622 + let cell_pointers page header = 623 + let ptrs = Array.make header.cell_count 0 in 624 + let ptr_start = page_header_size header.page_type in 625 + for i = 0 to header.cell_count - 1 do 626 + ptrs.(i) <- get_u16_be page (ptr_start + i * 2) 627 + done; 628 + ptrs 629 + 630 + let rec mem_in_page t page_num key = 631 + let page = Pager.read t.pager page_num in 632 + let header = parse_page_header page 0 in 633 + let ptrs = cell_pointers page header in 634 + let usable = usable_size t in 635 + match header.page_type with 636 + | Leaf_index -> 637 + let rec search i = 638 + if i >= header.cell_count then false 639 + else 640 + let cell, _ = Cell.parse_index_leaf page ptrs.(i) ~usable_size:usable in 641 + if cell.payload = key then true 642 + else if cell.payload > key then false 643 + else search (i + 1) 644 + in 645 + search 0 646 + | Interior_index -> 647 + let rec find_child i = 648 + if i >= header.cell_count then Option.get header.right_child 649 + else 650 + let cell, _ = Cell.parse_index_interior page ptrs.(i) ~usable_size:usable in 651 + if key <= cell.payload then cell.left_child else find_child (i + 1) 652 + in 653 + mem_in_page t (find_child 0) key 654 + | _ -> failwith "Invalid page type in index B-tree" 655 + 656 + let mem t key = mem_in_page t t.root_page key 657 + 658 + let insert _t _key = failwith "Index insert not yet implemented" 659 + let delete _t _key = failwith "Index delete not yet implemented" 660 + 661 + let iter t f = 662 + let rec iter_page page_num = 663 + let page = Pager.read t.pager page_num in 664 + let header = parse_page_header page 0 in 665 + let ptrs = cell_pointers page header in 666 + let usable = usable_size t in 667 + match header.page_type with 668 + | Leaf_index -> 669 + for i = 0 to header.cell_count - 1 do 670 + let cell, _ = Cell.parse_index_leaf page ptrs.(i) ~usable_size:usable in 671 + f cell.payload 672 + done 673 + | Interior_index -> 674 + for i = 0 to header.cell_count - 1 do 675 + let cell, _ = Cell.parse_index_interior page ptrs.(i) ~usable_size:usable in 676 + iter_page cell.left_child 677 + done; 678 + Option.iter iter_page header.right_child 679 + | _ -> failwith "Invalid page type" 680 + in 681 + iter_page t.root_page 682 + end
+224
lib/btree.mli
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Thomas Gazagnaire. All rights reserved. 3 + SPDX-License-Identifier: MIT 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** Pure OCaml B-tree for persistent storage. 7 + 8 + Implements SQLite-compatible B-tree pages for table and index storage. *) 9 + 10 + (** {1 Varint Encoding} 11 + 12 + Variable-length integers as used in SQLite. *) 13 + 14 + module Varint : sig 15 + val decode : string -> int -> int64 * int 16 + (** [decode buf off] decodes a varint starting at [off] in [buf]. 17 + Returns [(value, bytes_consumed)]. *) 18 + 19 + val encode : int64 -> string 20 + (** [encode n] encodes [n] as a varint. *) 21 + 22 + val size : int64 -> int 23 + (** [size n] returns the number of bytes needed to encode [n]. *) 24 + end 25 + 26 + (** {1 Page Types} *) 27 + 28 + type page_type = 29 + | Interior_index (** 0x02 *) 30 + | Interior_table (** 0x05 *) 31 + | Leaf_index (** 0x0a *) 32 + | Leaf_table (** 0x0d *) 33 + 34 + val pp_page_type : Format.formatter -> page_type -> unit 35 + 36 + (** {1 Page Header} *) 37 + 38 + type page_header = { 39 + page_type : page_type; 40 + first_freeblock : int; 41 + cell_count : int; 42 + cell_content_start : int; 43 + fragmented_bytes : int; 44 + right_child : int option; (** Interior pages only *) 45 + } 46 + 47 + val parse_page_header : string -> int -> page_header 48 + (** [parse_page_header buf off] parses a page header starting at [off]. 49 + For page 1, [off] should be 100 (after database header). *) 50 + 51 + val page_header_size : page_type -> int 52 + (** [page_header_size typ] is 8 for leaf pages, 12 for interior pages. *) 53 + 54 + (** {1 Cells} 55 + 56 + Cells contain the actual data in B-tree pages. *) 57 + 58 + module Cell : sig 59 + (** Table leaf cell: rowid + payload *) 60 + type table_leaf = { 61 + rowid : int64; 62 + payload : string; 63 + overflow_page : int option; 64 + } 65 + 66 + (** Table interior cell: child page + rowid *) 67 + type table_interior = { 68 + left_child : int; 69 + rowid : int64; 70 + } 71 + 72 + (** Index leaf cell: payload only *) 73 + type index_leaf = { 74 + payload : string; 75 + overflow_page : int option; 76 + } 77 + 78 + (** Index interior cell: child page + payload *) 79 + type index_interior = { 80 + left_child : int; 81 + payload : string; 82 + overflow_page : int option; 83 + } 84 + 85 + val parse_table_leaf : string -> int -> usable_size:int -> table_leaf * int 86 + (** [parse_table_leaf buf off ~usable_size] parses a table leaf cell. 87 + Returns [(cell, bytes_consumed)]. *) 88 + 89 + val parse_table_interior : string -> int -> table_interior * int 90 + (** [parse_table_interior buf off] parses a table interior cell. *) 91 + 92 + val parse_index_leaf : string -> int -> usable_size:int -> index_leaf * int 93 + (** [parse_index_leaf buf off ~usable_size] parses an index leaf cell. *) 94 + 95 + val parse_index_interior : string -> int -> usable_size:int -> index_interior * int 96 + (** [parse_index_interior buf off ~usable_size] parses an index interior cell. *) 97 + end 98 + 99 + (** {1 Record Format} 100 + 101 + SQLite record format for storing column values. *) 102 + 103 + module Record : sig 104 + (** Serial types determine how column values are stored. *) 105 + type serial_type = 106 + | Null 107 + | Int8 108 + | Int16 109 + | Int24 110 + | Int32 111 + | Int48 112 + | Int64 113 + | Float64 114 + | Zero 115 + | One 116 + | Blob of int 117 + | Text of int 118 + 119 + (** Column values *) 120 + type value = 121 + | Vnull 122 + | Vint of int64 123 + | Vfloat of float 124 + | Vblob of string 125 + | Vtext of string 126 + 127 + val decode : string -> value list 128 + (** [decode payload] decodes a record from its payload bytes. *) 129 + 130 + val encode : value list -> string 131 + (** [encode values] encodes values as a record. *) 132 + 133 + val pp_value : Format.formatter -> value -> unit 134 + end 135 + 136 + (** {1 Pager} 137 + 138 + Page I/O and caching layer. *) 139 + 140 + module Pager : sig 141 + type t 142 + 143 + val create : page_size:int -> Eio.File.rw_ty Eio.Resource.t -> t 144 + (** [create ~page_size file] creates a pager with the given page size. *) 145 + 146 + val page_size : t -> int 147 + (** [page_size t] returns the page size. *) 148 + 149 + val page_count : t -> int 150 + (** [page_count t] returns the number of pages in the file. *) 151 + 152 + val read : t -> int -> string 153 + (** [read t page_num] reads page [page_num] (1-indexed). *) 154 + 155 + val write : t -> int -> string -> unit 156 + (** [write t page_num data] writes [data] to page [page_num]. *) 157 + 158 + val allocate : t -> int 159 + (** [allocate t] allocates a new page and returns its number. *) 160 + 161 + val sync : t -> unit 162 + (** [sync t] syncs all dirty pages to disk. *) 163 + end 164 + 165 + (** {1 Table B-tree} 166 + 167 + B-tree for rowid-keyed tables (like SQLite tables). *) 168 + 169 + module Table : sig 170 + type t 171 + 172 + val create : Pager.t -> t 173 + (** [create pager] creates a new empty table B-tree. *) 174 + 175 + val open_ : Pager.t -> root_page:int -> t 176 + (** [open_ pager ~root_page] opens an existing table B-tree. *) 177 + 178 + val root_page : t -> int 179 + (** [root_page t] returns the root page number. *) 180 + 181 + val find : t -> int64 -> string option 182 + (** [find t rowid] finds the record with the given rowid. *) 183 + 184 + val insert : t -> rowid:int64 -> string -> unit 185 + (** [insert t ~rowid data] inserts or updates a record. *) 186 + 187 + val delete : t -> int64 -> unit 188 + (** [delete t rowid] deletes the record with the given rowid. *) 189 + 190 + val iter : t -> (int64 -> string -> unit) -> unit 191 + (** [iter t f] calls [f rowid data] for each record in order. *) 192 + 193 + val fold : t -> init:'a -> f:(int64 -> string -> 'a -> 'a) -> 'a 194 + (** [fold t ~init ~f] folds over all records in order. *) 195 + end 196 + 197 + (** {1 Index B-tree} 198 + 199 + B-tree for arbitrary keys (like SQLite indexes). *) 200 + 201 + module Index : sig 202 + type t 203 + 204 + val create : Pager.t -> t 205 + (** [create pager] creates a new empty index B-tree. *) 206 + 207 + val open_ : Pager.t -> root_page:int -> t 208 + (** [open_ pager ~root_page] opens an existing index B-tree. *) 209 + 210 + val root_page : t -> int 211 + (** [root_page t] returns the root page number. *) 212 + 213 + val mem : t -> string -> bool 214 + (** [mem t key] returns true if [key] exists in the index. *) 215 + 216 + val insert : t -> string -> unit 217 + (** [insert t key] inserts a key. *) 218 + 219 + val delete : t -> string -> unit 220 + (** [delete t key] deletes a key. *) 221 + 222 + val iter : t -> (string -> unit) -> unit 223 + (** [iter t f] calls [f key] for each key in order. *) 224 + end
+4
lib/dune
··· 1 + (library 2 + (name btree) 3 + (public_name btree) 4 + (libraries eio cstruct))
+3
test/dune
··· 1 + (test 2 + (name test) 3 + (libraries btree alcotest eio_main))
+1
test/test.ml
··· 1 + let () = Alcotest.run "btree" Test_btree.suite
+215
test/test_btree.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Thomas Gazagnaire. All rights reserved. 3 + SPDX-License-Identifier: MIT 4 + ---------------------------------------------------------------------------*) 5 + 6 + (* Varint tests *) 7 + 8 + let test_varint_roundtrip () = 9 + let test n = 10 + let encoded = Btree.Varint.encode n in 11 + let decoded, _ = Btree.Varint.decode encoded 0 in 12 + Alcotest.(check int64) (Printf.sprintf "roundtrip %Ld" n) n decoded 13 + in 14 + test 0L; 15 + test 1L; 16 + test 127L; 17 + test 128L; 18 + test 255L; 19 + test 16383L; 20 + test 16384L; 21 + test 1000000L; 22 + test Int64.max_int 23 + 24 + let test_varint_size () = 25 + Alcotest.(check int) "size 0" 1 (Btree.Varint.size 0L); 26 + Alcotest.(check int) "size 127" 1 (Btree.Varint.size 127L); 27 + Alcotest.(check int) "size 128" 2 (Btree.Varint.size 128L); 28 + Alcotest.(check int) "size 16383" 2 (Btree.Varint.size 16383L); 29 + Alcotest.(check int) "size 16384" 3 (Btree.Varint.size 16384L) 30 + 31 + (* Record format tests *) 32 + 33 + let test_record_null () = 34 + let values = [ Btree.Record.Vnull ] in 35 + let encoded = Btree.Record.encode values in 36 + let decoded = Btree.Record.decode encoded in 37 + Alcotest.(check int) "length" 1 (List.length decoded); 38 + match List.hd decoded with 39 + | Btree.Record.Vnull -> () 40 + | _ -> Alcotest.fail "expected Vnull" 41 + 42 + let test_record_integers () = 43 + let values = 44 + [ 45 + Btree.Record.Vint 0L; 46 + Btree.Record.Vint 1L; 47 + Btree.Record.Vint 42L; 48 + Btree.Record.Vint (-1L); 49 + Btree.Record.Vint 1000000L; 50 + ] 51 + in 52 + let encoded = Btree.Record.encode values in 53 + let decoded = Btree.Record.decode encoded in 54 + Alcotest.(check int) "length" 5 (List.length decoded); 55 + let check_int expected actual = 56 + match actual with 57 + | Btree.Record.Vint n -> Alcotest.(check int64) "int" expected n 58 + | _ -> Alcotest.fail "expected Vint" 59 + in 60 + check_int 0L (List.nth decoded 0); 61 + check_int 1L (List.nth decoded 1); 62 + check_int 42L (List.nth decoded 2); 63 + check_int (-1L) (List.nth decoded 3); 64 + check_int 1000000L (List.nth decoded 4) 65 + 66 + let test_record_text () = 67 + let values = [ Btree.Record.Vtext "Hello, World!" ] in 68 + let encoded = Btree.Record.encode values in 69 + let decoded = Btree.Record.decode encoded in 70 + match List.hd decoded with 71 + | Btree.Record.Vtext s -> Alcotest.(check string) "text" "Hello, World!" s 72 + | _ -> Alcotest.fail "expected Vtext" 73 + 74 + let test_record_blob () = 75 + let data = "\x00\x01\x02\xff" in 76 + let values = [ Btree.Record.Vblob data ] in 77 + let encoded = Btree.Record.encode values in 78 + let decoded = Btree.Record.decode encoded in 79 + match List.hd decoded with 80 + | Btree.Record.Vblob s -> Alcotest.(check string) "blob" data s 81 + | _ -> Alcotest.fail "expected Vblob" 82 + 83 + let test_record_mixed () = 84 + let values = 85 + [ 86 + Btree.Record.Vnull; 87 + Btree.Record.Vint 42L; 88 + Btree.Record.Vtext "test"; 89 + Btree.Record.Vblob "\xff\xfe"; 90 + ] 91 + in 92 + let encoded = Btree.Record.encode values in 93 + let decoded = Btree.Record.decode encoded in 94 + Alcotest.(check int) "length" 4 (List.length decoded) 95 + 96 + (* Page header tests *) 97 + 98 + let test_page_header_leaf () = 99 + let page = Bytes.create 4096 in 100 + Bytes.set_uint8 page 0 0x0d; (* Leaf table *) 101 + Bytes.set_uint8 page 1 0; 102 + Bytes.set_uint8 page 2 0; (* first freeblock = 0 *) 103 + Bytes.set_uint8 page 3 0; 104 + Bytes.set_uint8 page 4 5; (* cell count = 5 *) 105 + Bytes.set_uint8 page 5 0x0f; 106 + Bytes.set_uint8 page 6 0x00; (* cell content start = 0x0f00 *) 107 + Bytes.set_uint8 page 7 0; (* fragmented = 0 *) 108 + let header = Btree.parse_page_header (Bytes.unsafe_to_string page) 0 in 109 + Alcotest.(check int) "cell_count" 5 header.cell_count; 110 + Alcotest.(check int) "cell_content_start" 0x0f00 header.cell_content_start; 111 + Alcotest.(check bool) "no right_child" true (Option.is_none header.right_child) 112 + 113 + let test_page_header_interior () = 114 + let page = Bytes.create 4096 in 115 + Bytes.set_uint8 page 0 0x05; (* Interior table *) 116 + Bytes.set_uint8 page 1 0; 117 + Bytes.set_uint8 page 2 0; 118 + Bytes.set_uint8 page 3 0; 119 + Bytes.set_uint8 page 4 3; 120 + Bytes.set_uint8 page 5 0x10; 121 + Bytes.set_uint8 page 6 0x00; 122 + Bytes.set_uint8 page 7 0; 123 + (* Right child at offset 8-11 *) 124 + Bytes.set_uint8 page 8 0; 125 + Bytes.set_uint8 page 9 0; 126 + Bytes.set_uint8 page 10 0; 127 + Bytes.set_uint8 page 11 42; 128 + let header = Btree.parse_page_header (Bytes.unsafe_to_string page) 0 in 129 + Alcotest.(check int) "cell_count" 3 header.cell_count; 130 + match header.right_child with 131 + | Some n -> Alcotest.(check int) "right_child" 42 n 132 + | None -> Alcotest.fail "expected right_child" 133 + 134 + (* Table B-tree tests with file *) 135 + 136 + let with_temp_file f = 137 + Eio_main.run @@ fun env -> 138 + let cwd = Eio.Stdenv.cwd env in 139 + let tmp_dir = Eio.Path.(cwd / "_build" / "test_btree") in 140 + (try Eio.Path.mkdirs ~exists_ok:true ~perm:0o755 tmp_dir with _ -> ()); 141 + let path = 142 + Eio.Path.(tmp_dir / Printf.sprintf "test_%d.db" (Random.int 1_000_000)) 143 + in 144 + Eio.Path.with_open_out ~create:(`Or_truncate 0o644) path (fun file -> 145 + f (file :> Eio.File.rw_ty Eio.Resource.t)) 146 + 147 + let test_table_create () = 148 + with_temp_file @@ fun file -> 149 + let pager = Btree.Pager.create ~page_size:4096 file in 150 + let tree = Btree.Table.create pager in 151 + Alcotest.(check int) "root page" 1 (Btree.Table.root_page tree) 152 + 153 + let test_table_insert_find () = 154 + with_temp_file @@ fun file -> 155 + let pager = Btree.Pager.create ~page_size:4096 file in 156 + let tree = Btree.Table.create pager in 157 + Btree.Table.insert tree ~rowid:1L "Hello"; 158 + let result = Btree.Table.find tree 1L in 159 + Alcotest.(check (option string)) "found" (Some "Hello") result 160 + 161 + let test_table_multiple_inserts () = 162 + with_temp_file @@ fun file -> 163 + let pager = Btree.Pager.create ~page_size:4096 file in 164 + let tree = Btree.Table.create pager in 165 + Btree.Table.insert tree ~rowid:3L "Three"; 166 + Btree.Table.insert tree ~rowid:1L "One"; 167 + Btree.Table.insert tree ~rowid:2L "Two"; 168 + Alcotest.(check (option string)) "find 1" (Some "One") (Btree.Table.find tree 1L); 169 + Alcotest.(check (option string)) "find 2" (Some "Two") (Btree.Table.find tree 2L); 170 + Alcotest.(check (option string)) "find 3" (Some "Three") (Btree.Table.find tree 3L); 171 + Alcotest.(check (option string)) "find 4" None (Btree.Table.find tree 4L) 172 + 173 + let test_table_iter () = 174 + with_temp_file @@ fun file -> 175 + let pager = Btree.Pager.create ~page_size:4096 file in 176 + let tree = Btree.Table.create pager in 177 + Btree.Table.insert tree ~rowid:1L "A"; 178 + Btree.Table.insert tree ~rowid:2L "B"; 179 + Btree.Table.insert tree ~rowid:3L "C"; 180 + let items = ref [] in 181 + Btree.Table.iter tree (fun rowid data -> items := (rowid, data) :: !items); 182 + let sorted = List.sort compare !items in 183 + Alcotest.(check (list (pair int64 string))) 184 + "iter order" 185 + [ (1L, "A"); (2L, "B"); (3L, "C") ] 186 + sorted 187 + 188 + let suite = 189 + [ 190 + ( "varint", 191 + [ 192 + Alcotest.test_case "roundtrip" `Quick test_varint_roundtrip; 193 + Alcotest.test_case "size" `Quick test_varint_size; 194 + ] ); 195 + ( "record", 196 + [ 197 + Alcotest.test_case "null" `Quick test_record_null; 198 + Alcotest.test_case "integers" `Quick test_record_integers; 199 + Alcotest.test_case "text" `Quick test_record_text; 200 + Alcotest.test_case "blob" `Quick test_record_blob; 201 + Alcotest.test_case "mixed" `Quick test_record_mixed; 202 + ] ); 203 + ( "page_header", 204 + [ 205 + Alcotest.test_case "leaf" `Quick test_page_header_leaf; 206 + Alcotest.test_case "interior" `Quick test_page_header_interior; 207 + ] ); 208 + ( "table", 209 + [ 210 + Alcotest.test_case "create" `Quick test_table_create; 211 + Alcotest.test_case "insert/find" `Quick test_table_insert_find; 212 + Alcotest.test_case "multiple inserts" `Quick test_table_multiple_inserts; 213 + Alcotest.test_case "iter" `Quick test_table_iter; 214 + ] ); 215 + ]