123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247(** Alternating two-header commit protocol.
Pages 0 and 1 are the two header pages. On each commit, the "inactive"
header page is overwritten with the new state and the pager is flushed
(fsync). The header with the higher txn_id and valid CRC is live. *)typeencryption={canary_nonce:string;canary_tag:string}letenc_magic_value=0x53454E43ltypet={txn_id:int64;root_page:int64;freelist_page:int64;n_pages_total:int64;schema_version:int64;format_version:int32(** On-disk format version (#174). Preserved across commits; only a fresh
[init] stamps [current_format_version]. Opening a header whose version
exceeds [max_supported_format_version] fails with [Unsupported_format]. *);geom:Geometry.t(** Page geometry persisted in the header (#95): page_size at byte 56,
reserved_bytes_per_page at byte 64. Chosen at creation, immutable
thereafter, preserved verbatim across commits. *);enc:encryptionoption(** Encryption marker and key-check canary (#84). [None] for plaintext
databases; [Some _] when enc_magic = 0x53454E43 is set on disk. *)}(* On-disk format versions:
- v1: original layout, no schema fingerprints / mirror / page stamps.
- v2: #174 — schema fingerprints, redundant catalog mirror, per-page
fingerprint stamp in the reserved header bytes. *)letcurrent_format_version=2lletmax_supported_format_version=2ltypeerror=|Ioofstring|Both_headers_corrupt|Unsupported_formatofint32letpp_errorfmt=function|Iomsg->Format.fprintffmt"Io: %s"msg|Both_headers_corrupt->Format.pp_print_stringfmt"Both_headers_corrupt"|Unsupported_formatv->Format.fprintffmt"Unsupported_format: %ld"v;;letppfmtt=Format.fprintffmt"@[<hv>{ txn_id = %Ld;@ root_page = %Ld;@ freelist_page = %Ld;@ n_pages_total = \
%Ld;@ schema_version = %Ld }@]"t.txn_idt.root_paget.freelist_paget.n_pages_totalt.schema_version;;(* Convert a Pager error to our error type. *)letof_pager_erre=Io(Format.asprintf"%a"Pager.pp_errore)(* Build a sealed header page buffer. Sized to the file's geometry (#95). *)letbuild_page(h:t)=letbuf=Cstruct.createh.geom.page_sizeinPage.write_commonbuf{Page.kind=Page.Header;flags=0;n_keys=0;right_page=0l;crc32=0l};Page.write_header_fieldsbuf{Page.txn_id=h.txn_id;root_page=h.root_page;freelist_page=h.freelist_page;n_pages_total=h.n_pages_total;schema_version=h.schema_version;page_size=Int32.of_inth.geom.page_size;format_version=h.format_version;reserved_bytes_per_page=Int32.of_inth.geom.reserved_bytes_per_page;enc_magic=(matchh.encwith|Some_->enc_magic_value|None->0l);canary_nonce=(matchh.encwith|Somee->e.canary_nonce|None->String.make16'\000');canary_tag=(matchh.encwith|Somee->e.canary_tag|None->String.make16'\000')};Page.sealbuf;buf;;(* Try to decode a header from a raw page buffer.
Returns [Some t] when the page is a valid Header page with correct CRC,
[None] otherwise. *)letdecode_pagebuf=ifnot(Page.verify_crcbuf)thenNoneelse(matchPage.read_commonbufwith|exception_->None|c->ifc.Page.kind<>Page.HeaderthenNoneelse(letf=Page.read_header_fieldsbufin(* Reconstruct the persisted geometry (#95). A stored geometry that
fails validation is treated as corruption — the header is rejected. *)matchGeometry.create~page_size:(Int32.to_intf.Page.page_size)~reserved_bytes_per_page:(Int32.to_intf.Page.reserved_bytes_per_page)with|Error_->None|Okgeom->letenc=ifInt32.equalf.Page.enc_magicenc_magic_valuethenSome{canary_nonce=f.Page.canary_nonce;canary_tag=f.Page.canary_tag}elseNoneinSome{txn_id=f.Page.txn_id;root_page=f.Page.root_page;freelist_page=f.Page.freelist_page;n_pages_total=f.Page.n_pages_total;schema_version=f.Page.schema_version;format_version=f.Page.format_version;geom;enc}));;(* ------------------------------------------------------------------ *)(* Public interface *)(* ------------------------------------------------------------------ *)letread_livepager=let%lwtr0=Pager.readpager0Linlet%lwtr1=Pager.readpager1Linleth0=matchr0with|Error_->None|Okbuf->decode_pagebufinleth1=matchr1with|Error_->None|Okbuf->decode_pagebufinletchosen=matchh0,h1with|None,None->ErrorBoth_headers_corrupt|Someh,None->Okh|None,Someh->Okh|Somea,Someb->(* Both valid — pick the one with the higher txn_id.
In case of a tie (e.g. right after init) prefer page 0 (a). *)ifInt64.compareb.txn_ida.txn_id>0thenOkbelseOkainLwt.return(matchchosenwith|(Error_:(t,error)result)ase->e|Okh->(* Forward-compatibility gate (#174): refuse a database written by a
newer binary rather than misreading its layout. *)ifInt32.compareh.format_versionmax_supported_format_version>0thenError(Unsupported_formath.format_version)elseOkh);;(* #95 bootstrap: learn a file's geometry from the leading bytes of page 0
WITHOUT verifying the CRC — the CRC covers the whole real page, whose size
we don't yet know. The page_size/reserved fields live at bytes 56/64, always
within the first 4096 bytes regardless of the true page size, so a single
default-geometry read of page 0 is enough to discover them. Returns [None]
for a zeroed/fresh page (page_size 0 fails validation) or for garbage; the
caller then falls back to a caller-supplied or default geometry, and the
subsequent full {!read_live} CRC-verifies under the chosen geometry. *)letpeek_geometry(first_bytes:Cstruct.t):Geometry.toption=ifCstruct.lengthfirst_bytes<68thenNoneelse(letpage_size=Int32.to_int(Cstruct.BE.get_uint32first_bytes56)inletreserved=Int32.to_int(Cstruct.BE.get_uint32first_bytes64)inmatchGeometry.create~page_size~reserved_bytes_per_page:reservedwith|Okg->Someg|Error_->None);;(* Internal: write the next header into the inactive page slot. Returns
the prepared header value (so callers can mirror it into in-memory
state). Does NOT flush — callers choose [Pager.flush] (full sync) or
[Pager.flush_no_sync] (group commit). *)letstage_next_headerpager~prev_header~new_state=letinactive_page=Int64.to_int(Int64.rem(Int64.addprev_header.txn_id1L)2L)inletnext_txn_id=Int64.addprev_header.txn_id1Linleth={new_statewithtxn_id=next_txn_id}inletbuf=build_pagehinPager.writepager(Int64.of_intinactive_page)buf;h;;letcommitpager~prev_header~new_state=let_=stage_next_headerpager~prev_header~new_stateinmatch%lwtPager.flushpagerwith|Errore->Lwt.return(Error(of_pager_erre))|Ok()->Lwt.return(Ok());;letcommit_no_syncpager~prev_header~new_state=let_=stage_next_headerpager~prev_header~new_stateinmatch%lwtPager.flush_no_syncpagerwith|Errore->Lwt.return(Error(of_pager_erre))|Ok()->Lwt.return(Ok());;letinit?(enc=None)pager=letzero={txn_id=0L;root_page=0L;freelist_page=0L;n_pages_total=0L;schema_version=0L;format_version=current_format_version;geom=Pager.geompager;enc}inletbuf0=build_pagezeroinletbuf1=build_pagezeroinPager.writepager0Lbuf0;Pager.writepager1Lbuf1;match%lwtPager.flushpagerwith|Errore->Lwt.return(Error(of_pager_erre))|Ok()->Lwt.return(Ok());;[@@@ai_disclosure"ai-generated"][@@@ai_model"claude-opus-4-7"][@@@ai_provider"Anthropic"]