Minimal SQLite key-value store for OCaml
0
fork

Configure Feed

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

test(btree,sqlite): add SQLite file format spec test vectors

B-tree tests (56 total):
- Overflow thresholds: X=U-35, M=((U-12)*32/255)-23 for all page sizes
- Boundary: exact max_local (4061), X+1 (4062), multi-page overflow
- All valid page sizes (512..65536) with and without overflow
- Page type flags, header sizes, varint 9-byte encoding
- Record serial types (NULL, int8/16/24/32/48/64, float, text, blob)
- Stress: 50 cells of increasing size with overflow + splits

SQLite tests (56 total):
- Database header byte-level: magic string, page size, payload fractions
(64/32/32), schema format, text encoding (UTF-8), reserved bytes
- change_counter == version_valid_for validity check
- Page 1 B-tree header at offset 100 (type=0x0d, fragmented<=60)
- Overflow value roundtrip and persistence across close/reopen

+145
+145
test/test_sqlite.ml
··· 711 711 Alcotest.(check int64) "sum of values" 60L sum; 712 712 Sqlite.close t 713 713 714 + (* ---- SQLite file format spec test vectors ---- *) 715 + 716 + let with_temp_db_path f = 717 + Eio_main.run @@ fun env -> 718 + let cwd = Eio.Stdenv.cwd env in 719 + let tmp_dir = Eio.Path.(cwd / "_build" / "test_sqlite") in 720 + (try Eio.Path.mkdirs ~exists_ok:true ~perm:0o755 tmp_dir with Eio.Io _ -> ()); 721 + let path = Eio.Path.(tmp_dir / Fmt.str "spec_%d.db" (Random.int 1_000_000)) in 722 + Eio.Switch.run @@ fun sw -> 723 + let db = Sqlite.v ~sw path in 724 + Fun.protect ~finally:(fun () -> Sqlite.close db) (fun () -> f path db) 725 + 726 + (* Section 1.2: Database header byte-level verification *) 727 + let test_db_header_magic () = 728 + with_temp_db_path @@ fun path db -> 729 + Sqlite.sync db; 730 + let data = Eio.Path.load path in 731 + let magic = String.sub data 0 16 in 732 + Alcotest.(check string) "magic" "SQLite format 3\000" magic 733 + 734 + let test_db_header_fixed_values () = 735 + with_temp_db_path @@ fun path db -> 736 + Sqlite.sync db; 737 + let data = Eio.Path.load path in 738 + (* Offset 16-17: page size (4096 = 0x10 0x00) *) 739 + Alcotest.(check int) "page size hi" 0x10 (Char.code data.[16]); 740 + Alcotest.(check int) "page size lo" 0x00 (Char.code data.[17]); 741 + (* Offset 18: write version = 1 (legacy) *) 742 + Alcotest.(check int) "write version" 1 (Char.code data.[18]); 743 + (* Offset 19: read version = 1 (legacy) *) 744 + Alcotest.(check int) "read version" 1 (Char.code data.[19]); 745 + (* Offset 20: reserved bytes = 0 *) 746 + Alcotest.(check int) "reserved" 0 (Char.code data.[20]); 747 + (* Offset 21: max_embedded_payload_fraction = 64 (MUST be 64) *) 748 + Alcotest.(check int) "max payload fraction" 64 (Char.code data.[21]); 749 + (* Offset 22: min_embedded_payload_fraction = 32 (MUST be 32) *) 750 + Alcotest.(check int) "min payload fraction" 32 (Char.code data.[22]); 751 + (* Offset 23: leaf_payload_fraction = 32 (MUST be 32) *) 752 + Alcotest.(check int) "leaf payload fraction" 32 (Char.code data.[23]); 753 + (* Offset 44: schema format = 4 *) 754 + let schema_format = 755 + (Char.code data.[44] lsl 24) 756 + lor (Char.code data.[45] lsl 16) 757 + lor (Char.code data.[46] lsl 8) 758 + lor Char.code data.[47] 759 + in 760 + Alcotest.(check int) "schema format" 4 schema_format; 761 + (* Offset 56: text encoding = 1 (UTF-8) *) 762 + let encoding = 763 + (Char.code data.[56] lsl 24) 764 + lor (Char.code data.[57] lsl 16) 765 + lor (Char.code data.[58] lsl 8) 766 + lor Char.code data.[59] 767 + in 768 + Alcotest.(check int) "text encoding UTF-8" 1 encoding; 769 + (* Offset 72-91: reserved for expansion = all zeros *) 770 + for i = 72 to 91 do 771 + Alcotest.(check int) (Fmt.str "reserved byte %d" i) 0 (Char.code data.[i]) 772 + done 773 + 774 + let test_db_header_change_counter () = 775 + with_temp_db_path @@ fun path db -> 776 + Sqlite.put db "key" "value"; 777 + Sqlite.sync db; 778 + let data = Eio.Path.load path in 779 + let read_u32 off = 780 + (Char.code data.[off] lsl 24) 781 + lor (Char.code data.[off + 1] lsl 16) 782 + lor (Char.code data.[off + 2] lsl 8) 783 + lor Char.code data.[off + 3] 784 + in 785 + let change_counter = read_u32 24 in 786 + let version_valid_for = read_u32 92 in 787 + Alcotest.(check int) 788 + "change_counter == version_valid_for" change_counter version_valid_for 789 + 790 + (* Section 1.5: Page 1 B-tree header at offset 100 *) 791 + let test_page1_btree_header () = 792 + with_temp_db_path @@ fun path db -> 793 + Sqlite.sync db; 794 + let data = Eio.Path.load path in 795 + (* Offset 100: page type = 0x0d (leaf table) *) 796 + Alcotest.(check int) "page1 type" 0x0d (Char.code data.[100]); 797 + (* Offset 107: fragmented bytes <= 60 *) 798 + Alcotest.(check bool) "fragmented <= 60" true (Char.code data.[107] <= 60) 799 + 800 + (* Section 2.1: sqlite_schema table format — 801 + columns: type, name, tbl_name, rootpage, sql *) 802 + let test_sqlite_schema_format () = 803 + with_temp_db @@ fun _fs db -> 804 + let table = Sqlite.Table.create db ~name:"test_table" in 805 + Sqlite.Table.put table "key" "value"; 806 + let schemas = Sqlite.tables db in 807 + let names = 808 + List.map (fun (s : Sqlite.schema) -> s.tbl_name) schemas 809 + |> List.sort String.compare 810 + in 811 + (* Should have both the default kv table and test_table *) 812 + Alcotest.(check bool) "has test_table" true (List.mem "test_table" names) 813 + 814 + (* Overflow values in SQLite-compatible files *) 815 + let test_sqlite_overflow_values () = 816 + with_temp_db @@ fun _fs db -> 817 + (* Values larger than max_local (4061 for 4096-byte pages) *) 818 + let large = String.make 5000 'X' in 819 + Sqlite.put db "overflow_key" large; 820 + let result = Sqlite.find db "overflow_key" in 821 + Alcotest.(check (option string)) 822 + "overflow value roundtrip" (Some large) result 823 + 824 + let test_sqlite_overflow_persistence () = 825 + Eio_main.run @@ fun env -> 826 + let cwd = Eio.Stdenv.cwd env in 827 + let tmp_dir = Eio.Path.(cwd / "_build" / "test_sqlite") in 828 + (try Eio.Path.mkdirs ~exists_ok:true ~perm:0o755 tmp_dir with Eio.Io _ -> ()); 829 + let path = 830 + Eio.Path.(tmp_dir / Fmt.str "overflow_%d.db" (Random.int 1_000_000)) 831 + in 832 + let large = String.make 10000 'Y' in 833 + (* Write *) 834 + Eio.Switch.run (fun sw -> 835 + let db = Sqlite.v ~sw path in 836 + Sqlite.put db "big" large; 837 + Sqlite.close db); 838 + (* Read back *) 839 + Eio.Switch.run (fun sw -> 840 + let db = Sqlite.open_ ~sw path in 841 + let result = Sqlite.find db "big" in 842 + Alcotest.(check (option string)) "overflow persists" (Some large) result; 843 + Sqlite.close db) 844 + 714 845 let suite = 715 846 ( "sqlite", 716 847 List.concat ··· 794 925 test_integer_primary_key; 795 926 Alcotest.test_case "tables lists all" `Quick test_tables_lists_all; 796 927 Alcotest.test_case "fold table" `Quick test_fold_table; 928 + ]; 929 + [ 930 + Alcotest.test_case "spec header magic" `Quick test_db_header_magic; 931 + Alcotest.test_case "spec header values" `Quick 932 + test_db_header_fixed_values; 933 + Alcotest.test_case "spec change counter" `Quick 934 + test_db_header_change_counter; 935 + Alcotest.test_case "spec page1 btree" `Quick test_page1_btree_header; 936 + Alcotest.test_case "spec schema format" `Quick 937 + test_sqlite_schema_format; 938 + Alcotest.test_case "spec overflow values" `Quick 939 + test_sqlite_overflow_values; 940 + Alcotest.test_case "spec overflow persist" `Quick 941 + test_sqlite_overflow_persistence; 797 942 ]; 798 943 ] )