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