Skip to main content

lance_file/
version.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4use std::fmt::{Display, Formatter};
5
6use lance_core::deepsize::{Context, DeepSizeOf};
7use lance_core::{Error, Result};
8
9pub use lance_encoding::version::{
10    LEGACY_FORMAT_VERSION, LanceFileVersion, V2_FORMAT_2_0, V2_FORMAT_2_1, V2_FORMAT_2_2,
11    V2_FORMAT_2_3,
12};
13
14/// The exact persisted identity of a Lance file format.
15///
16/// Unlike [`LanceFileVersion`], this type cannot represent release selectors such as
17/// `stable` or `next`. Exact versions deliberately have no ordering because format
18/// capabilities are not implied by release order.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub enum ConcreteFileVersion {
21    /// The legacy v1 file format.
22    V1,
23    /// The v2.0 file format.
24    V2_0,
25    /// The v2.1 file format.
26    V2_1,
27    /// The v2.2 file format.
28    V2_2,
29    /// The v2.3 file format.
30    V2_3,
31}
32
33impl DeepSizeOf for ConcreteFileVersion {
34    fn deep_size_of_children(&self, _context: &mut Context) -> usize {
35        0
36    }
37}
38
39impl ConcreteFileVersion {
40    /// Decode the exact version string stored in a dataset manifest.
41    ///
42    /// Public selector aliases such as `legacy`, `0.3`, `stable`, and `next` are
43    /// intentionally rejected because manifests only store canonical exact versions.
44    pub fn from_manifest_string(value: &str) -> Result<Self> {
45        match value {
46            LEGACY_FORMAT_VERSION => Ok(Self::V1),
47            V2_FORMAT_2_0 => Ok(Self::V2_0),
48            V2_FORMAT_2_1 => Ok(Self::V2_1),
49            V2_FORMAT_2_2 => Ok(Self::V2_2),
50            V2_FORMAT_2_3 => Ok(Self::V2_3),
51            _ => Err(unknown_version(value)),
52        }
53    }
54
55    /// Encode this exact version as the canonical string stored in a dataset manifest.
56    pub const fn to_manifest_string(self) -> &'static str {
57        match self {
58            Self::V1 => LEGACY_FORMAT_VERSION,
59            Self::V2_0 => V2_FORMAT_2_0,
60            Self::V2_1 => V2_FORMAT_2_1,
61            Self::V2_2 => V2_FORMAT_2_2,
62            Self::V2_3 => V2_FORMAT_2_3,
63        }
64    }
65
66    /// Decode the major/minor version stored in `DataFile` metadata.
67    ///
68    /// Legacy manifests may omit these fields and decode to `(0, 0)`, so all legacy
69    /// v1 number pairs accepted by the historical decoder remain valid inputs. The
70    /// historical generic decoder also accepted the standard v2.0 footer pair `(0, 3)`;
71    /// decoding retains that compatibility while encoding always emits `(2, 0)`.
72    pub fn from_data_file_numbers(major: u32, minor: u32) -> Result<Self> {
73        match (major, minor) {
74            (0, 0..=2) => Ok(Self::V1),
75            (0, 3) | (2, 0) => Ok(Self::V2_0),
76            (2, 1) => Ok(Self::V2_1),
77            (2, 2) => Ok(Self::V2_2),
78            (2, 3) => Ok(Self::V2_3),
79            _ => Err(unknown_version(format_args!("{}.{}", major, minor))),
80        }
81    }
82
83    /// Encode the canonical major/minor pair stored in `DataFile` metadata.
84    pub const fn to_data_file_numbers(self) -> (u32, u32) {
85        match self {
86            Self::V1 => (0, 2),
87            Self::V2_0 => (2, 0),
88            Self::V2_1 => (2, 1),
89            Self::V2_2 => (2, 2),
90            Self::V2_3 => (2, 3),
91        }
92    }
93
94    /// Decode the major/minor version stored in a Lance file footer.
95    ///
96    /// V2.0 has two accepted representations: `(0, 3)` from the standard file writer
97    /// and `(2, 0)` from self-described and mini-lance writers.
98    pub fn from_footer_numbers(major: u16, minor: u16) -> Result<Self> {
99        match (major, minor) {
100            (0, 0..=2) => Ok(Self::V1),
101            (0, 3) | (2, 0) => Ok(Self::V2_0),
102            (2, 1) => Ok(Self::V2_1),
103            (2, 2) => Ok(Self::V2_2),
104            (2, 3) => Ok(Self::V2_3),
105            _ => Err(unknown_version(format_args!("{}.{}", major, minor))),
106        }
107    }
108
109    /// Encode the footer numbers emitted by the standard Lance file writer.
110    pub const fn to_standard_footer_numbers(self) -> (u16, u16) {
111        match self {
112            Self::V1 => (0, 2),
113            Self::V2_0 => (0, 3),
114            Self::V2_1 => (2, 1),
115            Self::V2_2 => (2, 2),
116            Self::V2_3 => (2, 3),
117        }
118    }
119
120    /// Encode the footer numbers emitted by self-described and mini-lance writers.
121    pub const fn to_embedded_footer_numbers(self) -> (u16, u16) {
122        match self {
123            Self::V1 => (0, 2),
124            Self::V2_0 => (2, 0),
125            Self::V2_1 => (2, 1),
126            Self::V2_2 => (2, 2),
127            Self::V2_3 => (2, 3),
128        }
129    }
130}
131
132impl Display for ConcreteFileVersion {
133    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
134        f.write_str(self.to_manifest_string())
135    }
136}
137
138impl From<ConcreteFileVersion> for LanceFileVersion {
139    fn from(value: ConcreteFileVersion) -> Self {
140        match value {
141            ConcreteFileVersion::V1 => Self::Legacy,
142            ConcreteFileVersion::V2_0 => Self::V2_0,
143            ConcreteFileVersion::V2_1 => Self::V2_1,
144            ConcreteFileVersion::V2_2 => Self::V2_2,
145            ConcreteFileVersion::V2_3 => Self::V2_3,
146        }
147    }
148}
149
150impl From<LanceFileVersion> for ConcreteFileVersion {
151    fn from(value: LanceFileVersion) -> Self {
152        match value.resolve() {
153            LanceFileVersion::Legacy => Self::V1,
154            LanceFileVersion::V2_0 => Self::V2_0,
155            LanceFileVersion::V2_1 => Self::V2_1,
156            LanceFileVersion::V2_2 => Self::V2_2,
157            LanceFileVersion::V2_3 => Self::V2_3,
158            LanceFileVersion::Stable | LanceFileVersion::Next => {
159                unreachable!("resolved file-version selector must be exact")
160            }
161        }
162    }
163}
164
165fn unknown_version(value: impl Display) -> Error {
166    Error::invalid_input_source(format!("Unknown Lance storage version: {}", value).into())
167}
168
169#[cfg(test)]
170mod tests {
171    use std::str::FromStr;
172
173    use lance_io::object_store::ObjectStore;
174    use object_store::path::Path;
175
176    use super::*;
177
178    const EXACT_VERSIONS: [ConcreteFileVersion; 5] = [
179        ConcreteFileVersion::V1,
180        ConcreteFileVersion::V2_0,
181        ConcreteFileVersion::V2_1,
182        ConcreteFileVersion::V2_2,
183        ConcreteFileVersion::V2_3,
184    ];
185
186    #[test]
187    fn selector_resolution_is_exact() {
188        let cases = [
189            (LanceFileVersion::Legacy, ConcreteFileVersion::V1),
190            (LanceFileVersion::V2_0, ConcreteFileVersion::V2_0),
191            (LanceFileVersion::V2_1, ConcreteFileVersion::V2_1),
192            (LanceFileVersion::Stable, ConcreteFileVersion::V2_1),
193            (LanceFileVersion::V2_2, ConcreteFileVersion::V2_2),
194            (LanceFileVersion::Next, ConcreteFileVersion::V2_3),
195            (LanceFileVersion::V2_3, ConcreteFileVersion::V2_3),
196        ];
197
198        for (selector, expected) in cases {
199            assert_eq!(ConcreteFileVersion::from(selector), expected);
200            assert_eq!(LanceFileVersion::from(expected), selector.resolve());
201        }
202    }
203
204    #[test]
205    fn public_selector_aliases_remain_unchanged() {
206        let cases = [
207            ("0.1", LanceFileVersion::Legacy),
208            ("legacy", LanceFileVersion::Legacy),
209            ("2.0", LanceFileVersion::V2_0),
210            ("0.3", LanceFileVersion::V2_0),
211            ("2.1", LanceFileVersion::V2_1),
212            ("stable", LanceFileVersion::Stable),
213            ("2.2", LanceFileVersion::V2_2),
214            ("next", LanceFileVersion::Next),
215            ("2.3", LanceFileVersion::V2_3),
216        ];
217
218        for (value, expected) in cases {
219            assert_eq!(LanceFileVersion::from_str(value).unwrap(), expected);
220        }
221    }
222
223    #[test]
224    fn manifest_codec_only_accepts_canonical_exact_versions() {
225        for version in EXACT_VERSIONS {
226            let encoded = version.to_manifest_string();
227            assert_eq!(
228                ConcreteFileVersion::from_manifest_string(encoded).unwrap(),
229                version
230            );
231        }
232
233        for selector_or_alias in ["legacy", "0.3", "stable", "next"] {
234            assert!(ConcreteFileVersion::from_manifest_string(selector_or_alias).is_err());
235        }
236    }
237
238    #[test]
239    fn data_file_codec_preserves_wire_numbers() {
240        let cases = [
241            (ConcreteFileVersion::V1, (0, 2)),
242            (ConcreteFileVersion::V2_0, (2, 0)),
243            (ConcreteFileVersion::V2_1, (2, 1)),
244            (ConcreteFileVersion::V2_2, (2, 2)),
245            (ConcreteFileVersion::V2_3, (2, 3)),
246        ];
247
248        for (version, encoded) in cases {
249            assert_eq!(version.to_data_file_numbers(), encoded);
250            assert_eq!(
251                ConcreteFileVersion::from_data_file_numbers(encoded.0, encoded.1).unwrap(),
252                version
253            );
254        }
255        for minor in 0..=2 {
256            assert_eq!(
257                ConcreteFileVersion::from_data_file_numbers(0, minor).unwrap(),
258                ConcreteFileVersion::V1
259            );
260        }
261        assert_eq!(
262            ConcreteFileVersion::from_data_file_numbers(0, 3).unwrap(),
263            ConcreteFileVersion::V2_0
264        );
265    }
266
267    #[test]
268    fn footer_codec_preserves_both_v2_0_writer_representations() {
269        let standard_cases = [
270            (ConcreteFileVersion::V1, (0, 2)),
271            (ConcreteFileVersion::V2_0, (0, 3)),
272            (ConcreteFileVersion::V2_1, (2, 1)),
273            (ConcreteFileVersion::V2_2, (2, 2)),
274            (ConcreteFileVersion::V2_3, (2, 3)),
275        ];
276        let embedded_cases = [
277            (ConcreteFileVersion::V1, (0, 2)),
278            (ConcreteFileVersion::V2_0, (2, 0)),
279            (ConcreteFileVersion::V2_1, (2, 1)),
280            (ConcreteFileVersion::V2_2, (2, 2)),
281            (ConcreteFileVersion::V2_3, (2, 3)),
282        ];
283
284        for (version, encoded) in standard_cases {
285            assert_eq!(version.to_standard_footer_numbers(), encoded);
286            assert_eq!(
287                ConcreteFileVersion::from_footer_numbers(encoded.0, encoded.1).unwrap(),
288                version
289            );
290        }
291        for (version, encoded) in embedded_cases {
292            assert_eq!(version.to_embedded_footer_numbers(), encoded);
293            assert_eq!(
294                ConcreteFileVersion::from_footer_numbers(encoded.0, encoded.1).unwrap(),
295                version
296            );
297        }
298        for minor in 0..=2 {
299            assert_eq!(
300                ConcreteFileVersion::from_footer_numbers(0, minor).unwrap(),
301                ConcreteFileVersion::V1
302            );
303        }
304    }
305
306    #[tokio::test]
307    async fn file_version_detection_accepts_all_legacy_footer_aliases() {
308        let object_store = ObjectStore::memory();
309        for minor in 0u16..=2 {
310            let path = Path::from(format!("legacy-{minor}.lance"));
311            let mut footer = Vec::with_capacity(8);
312            footer.extend_from_slice(&0u16.to_le_bytes());
313            footer.extend_from_slice(&minor.to_le_bytes());
314            footer.extend_from_slice(crate::format::MAGIC);
315            object_store.put(&path, &footer).await.unwrap();
316
317            assert_eq!(
318                crate::determine_file_version(&object_store, &path, Some(footer.len()))
319                    .await
320                    .unwrap(),
321                LanceFileVersion::Legacy
322            );
323        }
324    }
325}