1use std::{
5 fmt::{Display, Formatter},
6 str::FromStr,
7};
8
9use lance_core::deepsize::{Context, DeepSizeOf};
10use lance_core::{Error, Result};
11use serde::{Deserialize, Deserializer, Serialize, Serializer};
12
13pub const LEGACY_FORMAT_VERSION: &str = "0.1";
14pub const V2_FORMAT_2_0: &str = "2.0";
15pub const V2_FORMAT_2_1: &str = "2.1";
16pub const V2_FORMAT_2_2: &str = "2.2";
17pub const V2_FORMAT_2_3: &str = "2.3";
18
19pub const fn stable_file_version() -> ConcreteFileVersion {
21 ConcreteFileVersion::V2_2
22}
23
24pub const fn next_file_version() -> ConcreteFileVersion {
26 ConcreteFileVersion::V2_3
27}
28
29#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
34pub enum LanceFileVersion {
35 Legacy,
37 V2_0,
39 V2_1,
41 Stable,
43 #[default]
45 V2_2,
46 Next,
48 V2_3,
50}
51
52impl DeepSizeOf for LanceFileVersion {
53 fn deep_size_of_children(&self, _context: &mut Context) -> usize {
54 0
55 }
56}
57
58impl LanceFileVersion {
59 pub const fn resolve(self) -> ConcreteFileVersion {
61 match self {
62 Self::Legacy => ConcreteFileVersion::V1,
63 Self::V2_0 => ConcreteFileVersion::V2_0,
64 Self::V2_1 => ConcreteFileVersion::V2_1,
65 Self::Stable => stable_file_version(),
66 Self::V2_2 => ConcreteFileVersion::V2_2,
67 Self::Next => next_file_version(),
68 Self::V2_3 => ConcreteFileVersion::V2_3,
69 }
70 }
71
72 pub const fn is_unstable(self) -> bool {
74 self.resolve().is_unstable()
75 }
76}
77
78impl Display for LanceFileVersion {
79 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
80 f.write_str(match self {
81 Self::Legacy => LEGACY_FORMAT_VERSION,
82 Self::V2_0 => V2_FORMAT_2_0,
83 Self::V2_1 => V2_FORMAT_2_1,
84 Self::V2_2 => V2_FORMAT_2_2,
85 Self::V2_3 => V2_FORMAT_2_3,
86 Self::Stable => "stable",
87 Self::Next => "next",
88 })
89 }
90}
91
92impl FromStr for LanceFileVersion {
93 type Err = Error;
94
95 fn from_str(value: &str) -> Result<Self> {
96 match value.to_lowercase().as_str() {
97 LEGACY_FORMAT_VERSION | "legacy" => Ok(Self::Legacy),
98 V2_FORMAT_2_0 | "0.3" => Ok(Self::V2_0),
99 V2_FORMAT_2_1 => Ok(Self::V2_1),
100 V2_FORMAT_2_2 => Ok(Self::V2_2),
101 V2_FORMAT_2_3 => Ok(Self::V2_3),
102 "stable" => Ok(Self::Stable),
103 "next" => Ok(Self::Next),
104 _ => Err(unknown_version(value)),
105 }
106 }
107}
108
109impl Serialize for LanceFileVersion {
110 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
111 where
112 S: Serializer,
113 {
114 serializer.serialize_str(&self.to_string())
115 }
116}
117
118impl<'de> Deserialize<'de> for LanceFileVersion {
119 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
120 where
121 D: Deserializer<'de>,
122 {
123 let value = String::deserialize(deserializer)?;
124 Self::from_str(&value).map_err(serde::de::Error::custom)
125 }
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
134pub enum ConcreteFileVersion {
135 V1,
137 V2_0,
139 V2_1,
141 V2_2,
143 V2_3,
145}
146
147impl DeepSizeOf for ConcreteFileVersion {
148 fn deep_size_of_children(&self, _context: &mut Context) -> usize {
149 0
150 }
151}
152
153impl ConcreteFileVersion {
154 pub const fn to_selector(self) -> LanceFileVersion {
158 match self {
159 Self::V1 => LanceFileVersion::Legacy,
160 Self::V2_0 => LanceFileVersion::V2_0,
161 Self::V2_1 => LanceFileVersion::V2_1,
162 Self::V2_2 => LanceFileVersion::V2_2,
163 Self::V2_3 => LanceFileVersion::V2_3,
164 }
165 }
166
167 pub const fn is_unstable(self) -> bool {
169 matches!(self, Self::V2_3)
170 }
171
172 pub fn from_manifest_string(value: &str) -> Result<Self> {
177 match value {
178 LEGACY_FORMAT_VERSION => Ok(Self::V1),
179 V2_FORMAT_2_0 => Ok(Self::V2_0),
180 V2_FORMAT_2_1 => Ok(Self::V2_1),
181 V2_FORMAT_2_2 => Ok(Self::V2_2),
182 V2_FORMAT_2_3 => Ok(Self::V2_3),
183 _ => Err(unknown_version(value)),
184 }
185 }
186
187 pub const fn to_manifest_string(self) -> &'static str {
189 match self {
190 Self::V1 => LEGACY_FORMAT_VERSION,
191 Self::V2_0 => V2_FORMAT_2_0,
192 Self::V2_1 => V2_FORMAT_2_1,
193 Self::V2_2 => V2_FORMAT_2_2,
194 Self::V2_3 => V2_FORMAT_2_3,
195 }
196 }
197
198 pub fn from_data_file_numbers(major: u32, minor: u32) -> Result<Self> {
205 match (major, minor) {
206 (0, 0..=2) => Ok(Self::V1),
207 (0, 3) | (2, 0) => Ok(Self::V2_0),
208 (2, 1) => Ok(Self::V2_1),
209 (2, 2) => Ok(Self::V2_2),
210 (2, 3) => Ok(Self::V2_3),
211 _ => Err(unknown_version(format_args!("{}.{}", major, minor))),
212 }
213 }
214
215 pub const fn to_data_file_numbers(self) -> (u32, u32) {
217 match self {
218 Self::V1 => (0, 2),
219 Self::V2_0 => (2, 0),
220 Self::V2_1 => (2, 1),
221 Self::V2_2 => (2, 2),
222 Self::V2_3 => (2, 3),
223 }
224 }
225
226 pub fn from_footer_numbers(major: u16, minor: u16) -> Result<Self> {
231 match (major, minor) {
232 (0, 0..=2) => Ok(Self::V1),
233 (0, 3) | (2, 0) => Ok(Self::V2_0),
234 (2, 1) => Ok(Self::V2_1),
235 (2, 2) => Ok(Self::V2_2),
236 (2, 3) => Ok(Self::V2_3),
237 _ => Err(unknown_version(format_args!("{}.{}", major, minor))),
238 }
239 }
240
241 pub const fn to_standard_footer_numbers(self) -> (u16, u16) {
243 match self {
244 Self::V1 => (0, 2),
245 Self::V2_0 => (0, 3),
246 Self::V2_1 => (2, 1),
247 Self::V2_2 => (2, 2),
248 Self::V2_3 => (2, 3),
249 }
250 }
251
252 pub const fn to_embedded_footer_numbers(self) -> (u16, u16) {
254 match self {
255 Self::V1 => (0, 2),
256 Self::V2_0 => (2, 0),
257 Self::V2_1 => (2, 1),
258 Self::V2_2 => (2, 2),
259 Self::V2_3 => (2, 3),
260 }
261 }
262}
263
264impl Display for ConcreteFileVersion {
265 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
266 f.write_str(self.to_manifest_string())
267 }
268}
269
270fn unknown_version(value: impl Display) -> Error {
271 Error::invalid_input_source(format!("Unknown Lance storage version: {}", value).into())
272}
273
274#[cfg(test)]
275mod tests {
276 use std::str::FromStr;
277
278 use lance_io::object_store::ObjectStore;
279 use object_store::path::Path;
280
281 use super::*;
282
283 const EXACT_VERSIONS: [ConcreteFileVersion; 5] = [
284 ConcreteFileVersion::V1,
285 ConcreteFileVersion::V2_0,
286 ConcreteFileVersion::V2_1,
287 ConcreteFileVersion::V2_2,
288 ConcreteFileVersion::V2_3,
289 ];
290
291 #[test]
292 fn selector_resolution_is_exact() {
293 let cases = [
294 (LanceFileVersion::Legacy, ConcreteFileVersion::V1),
295 (LanceFileVersion::V2_0, ConcreteFileVersion::V2_0),
296 (LanceFileVersion::V2_1, ConcreteFileVersion::V2_1),
297 (LanceFileVersion::Stable, ConcreteFileVersion::V2_2),
298 (LanceFileVersion::V2_2, ConcreteFileVersion::V2_2),
299 (LanceFileVersion::Next, ConcreteFileVersion::V2_3),
300 (LanceFileVersion::V2_3, ConcreteFileVersion::V2_3),
301 ];
302
303 for (selector, expected) in cases {
304 assert_eq!(selector.resolve(), expected);
305 }
306 assert_eq!(LanceFileVersion::default(), LanceFileVersion::V2_2);
307 }
308
309 #[test]
310 fn public_selector_aliases_remain_unchanged() {
311 let cases = [
312 ("0.1", LanceFileVersion::Legacy),
313 ("legacy", LanceFileVersion::Legacy),
314 ("2.0", LanceFileVersion::V2_0),
315 ("0.3", LanceFileVersion::V2_0),
316 ("2.1", LanceFileVersion::V2_1),
317 ("stable", LanceFileVersion::Stable),
318 ("2.2", LanceFileVersion::V2_2),
319 ("next", LanceFileVersion::Next),
320 ("2.3", LanceFileVersion::V2_3),
321 ];
322
323 for (value, expected) in cases {
324 assert_eq!(LanceFileVersion::from_str(value).unwrap(), expected);
325 }
326 }
327
328 #[test]
329 fn manifest_codec_only_accepts_canonical_exact_versions() {
330 for version in EXACT_VERSIONS {
331 let encoded = version.to_manifest_string();
332 assert_eq!(
333 ConcreteFileVersion::from_manifest_string(encoded).unwrap(),
334 version
335 );
336 }
337
338 for selector_or_alias in ["legacy", "0.3", "stable", "next"] {
339 assert!(ConcreteFileVersion::from_manifest_string(selector_or_alias).is_err());
340 }
341 }
342
343 #[test]
344 fn data_file_codec_preserves_wire_numbers() {
345 let cases = [
346 (ConcreteFileVersion::V1, (0, 2)),
347 (ConcreteFileVersion::V2_0, (2, 0)),
348 (ConcreteFileVersion::V2_1, (2, 1)),
349 (ConcreteFileVersion::V2_2, (2, 2)),
350 (ConcreteFileVersion::V2_3, (2, 3)),
351 ];
352
353 for (version, encoded) in cases {
354 assert_eq!(version.to_data_file_numbers(), encoded);
355 assert_eq!(
356 ConcreteFileVersion::from_data_file_numbers(encoded.0, encoded.1).unwrap(),
357 version
358 );
359 }
360 for minor in 0..=2 {
361 assert_eq!(
362 ConcreteFileVersion::from_data_file_numbers(0, minor).unwrap(),
363 ConcreteFileVersion::V1
364 );
365 }
366 assert_eq!(
367 ConcreteFileVersion::from_data_file_numbers(0, 3).unwrap(),
368 ConcreteFileVersion::V2_0
369 );
370 }
371
372 #[test]
373 fn footer_codec_preserves_both_v2_0_writer_representations() {
374 let standard_cases = [
375 (ConcreteFileVersion::V1, (0, 2)),
376 (ConcreteFileVersion::V2_0, (0, 3)),
377 (ConcreteFileVersion::V2_1, (2, 1)),
378 (ConcreteFileVersion::V2_2, (2, 2)),
379 (ConcreteFileVersion::V2_3, (2, 3)),
380 ];
381 let embedded_cases = [
382 (ConcreteFileVersion::V1, (0, 2)),
383 (ConcreteFileVersion::V2_0, (2, 0)),
384 (ConcreteFileVersion::V2_1, (2, 1)),
385 (ConcreteFileVersion::V2_2, (2, 2)),
386 (ConcreteFileVersion::V2_3, (2, 3)),
387 ];
388
389 for (version, encoded) in standard_cases {
390 assert_eq!(version.to_standard_footer_numbers(), encoded);
391 assert_eq!(
392 ConcreteFileVersion::from_footer_numbers(encoded.0, encoded.1).unwrap(),
393 version
394 );
395 }
396 for (version, encoded) in embedded_cases {
397 assert_eq!(version.to_embedded_footer_numbers(), encoded);
398 assert_eq!(
399 ConcreteFileVersion::from_footer_numbers(encoded.0, encoded.1).unwrap(),
400 version
401 );
402 }
403 for minor in 0..=2 {
404 assert_eq!(
405 ConcreteFileVersion::from_footer_numbers(0, minor).unwrap(),
406 ConcreteFileVersion::V1
407 );
408 }
409 }
410
411 #[tokio::test]
412 async fn file_version_detection_accepts_all_legacy_footer_aliases() {
413 let object_store = ObjectStore::memory();
414 for minor in 0u16..=2 {
415 let path = Path::from(format!("legacy-{minor}.lance"));
416 let mut footer = Vec::with_capacity(8);
417 footer.extend_from_slice(&0u16.to_le_bytes());
418 footer.extend_from_slice(&minor.to_le_bytes());
419 footer.extend_from_slice(crate::format::MAGIC);
420 object_store.put(&path, &footer).await.unwrap();
421
422 assert_eq!(
423 crate::determine_file_version(&object_store, &path, Some(footer.len()))
424 .await
425 .unwrap(),
426 ConcreteFileVersion::V1
427 );
428 }
429 }
430}