lsm_tree/format_version.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2024-present, fjall-rs
3// Copyright (c) 2026-present, Structured World Foundation
4
5/// Block / SST disk format version.
6///
7/// This enum tracks the on-disk layout of Blocks and SST files: block
8/// header layout, filter wire format, range-tombstone encoding, ECC
9/// trailer geometry. It is the version persisted in the manifest's
10/// `format_version` section and gated at `Tree::open`.
11///
12/// ## Relationship to the manifest layout version
13///
14/// `FormatVersion` and [`crate::manifest_blocks::MANIFEST_LAYOUT_VERSION_V1`]
15/// evolve at **independent cadences**:
16///
17/// | Concept | Type | Tracks |
18/// |---------|------|--------|
19/// | `FormatVersion` | This enum (V1..V5) | Block / SST on-disk layout |
20/// | `manifest_layout_version` | `u8` in manifest Footer Block | Manifest file structure (footer fields, TOC encoding, head-mirror geometry) |
21///
22/// A block format bump does NOT force a manifest layout bump and
23/// vice versa. The CURRENT pointer's canonical digest binds the
24/// manifest layout version (so a manifest-only break is detected
25/// at recovery), and the manifest's `format_version` section binds
26/// this enum (so a block-format-only break is detected at
27/// `Tree::open`).
28///
29/// ## Amendment policy
30///
31/// Once a value is **published to crates.io** (any released binary
32/// writes that value to disk), **any** subsequent change to the
33/// on-disk bytes under that value is a breaking change that MUST
34/// bump to a new variant. This applies regardless of whether the
35/// change is otherwise additive: a reader running the old code is
36/// not free to interpret unknown bytes.
37///
38/// The amendment window is the **pre-release period**: while a
39/// `FormatVersion` is being actively developed and no published
40/// binary writes it, the on-disk bytes under that version MAY be
41/// amended in place (no enum bump required). The release that
42/// crystallises the variant ends this window.
43///
44/// Same rule applies to `manifest_layout_version` independently:
45/// pre-publication amendments are free; post-publication changes
46/// require a new layout-version constant.
47///
48/// **Practical checklist for any PR that touches on-disk bytes:**
49///
50/// 1. Identify which layer the change touches (Block/SST → this
51/// enum; manifest framing → `manifest_layout_version`).
52/// 2. If that layer's current value has shipped to crates.io,
53/// add a new variant / constant instead of amending in place.
54/// 3. The OTHER layer's value stays unless its layer also changed.
55#[derive(Copy, Clone, Debug, Eq, PartialEq)]
56pub enum FormatVersion {
57 /// Version for 1.x.x releases
58 V1 = 1,
59
60 /// Version for 2.x.x releases
61 V2,
62
63 /// Version for 3.x.x releases
64 V3,
65
66 /// Version for range-tombstone SST semantics
67 V4,
68
69 /// Two on-disk changes shipped together in this format version
70 /// (V5 had not been released when both landed, so they collapse
71 /// into the same version bump):
72 ///
73 /// 1. `BuRR` (Bumped Ribbon Retrieval) filter wire format. Filter
74 /// blocks are no longer Bloom-encoded; the `filter_type` byte +
75 /// per-layer header layout is documented in
76 /// `src/table/filter/ribbon/burr/wire.rs`.
77 ///
78 /// 2. Per-block transform flags + Page ECC. The self-describing block
79 /// types (`Meta` / `Manifest` / `ManifestFooter`) carry a
80 /// `block_flags: u8` byte with the transform-presence bits;
81 /// `ECC_PARITY` marks that a Reed-Solomon parity trailer follows
82 /// the XXH3-covered payload (its length is derived from
83 /// `data_length`, not stored). SST block types (`Data` / `Index` /
84 /// `Filter` / `RangeTombstone`) keep the compact header WITHOUT this
85 /// byte: their parity / per-KV-footer presence is a per-SST property
86 /// read from the table descriptor (`page_ecc` / `kv_checksum_algo`),
87 /// not a serialized header flag. `KV_CHECKSUM_FOOTER` (set on the
88 /// self-describing types) marks a per-entry checksum footer.
89 /// When `Config::page_ecc(false)` (the default) no parity bytes
90 /// follow; likewise no footer unless per-KV checksums are enabled.
91 /// The block
92 /// magic was bumped to `[L,S,M,4]` (was `[L,S,M,3]` on pre-V5
93 /// versions) so a pre-V5 reader that bypasses the manifest gate
94 /// fails fast at block header decode rather than misreading the
95 /// new layout.
96 ///
97 /// V3 / V4 ↔ V5 incompatibility is enforced primarily by the
98 /// manifest version gate at `Tree::open` (returns
99 /// `InvalidVersion` for anything other than V5).
100 V5,
101}
102
103impl core::fmt::Display for FormatVersion {
104 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
105 write!(f, "{}", u8::from(*self))
106 }
107}
108
109impl From<FormatVersion> for u8 {
110 fn from(value: FormatVersion) -> Self {
111 match value {
112 FormatVersion::V1 => 1,
113 FormatVersion::V2 => 2,
114 FormatVersion::V3 => 3,
115 FormatVersion::V4 => 4,
116 FormatVersion::V5 => 5,
117 }
118 }
119}
120
121impl TryFrom<u8> for FormatVersion {
122 type Error = ();
123
124 fn try_from(value: u8) -> Result<Self, Self::Error> {
125 match value {
126 1 => Ok(Self::V1),
127 2 => Ok(Self::V2),
128 3 => Ok(Self::V3),
129 4 => Ok(Self::V4),
130 5 => Ok(Self::V5),
131 _ => Err(()),
132 }
133 }
134}